← back to Bertha

react-dash/server.js

696 lines

import http from 'http';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import pg from 'pg';
import crypto from 'crypto';
import { OAuth2Client } from 'google-auth-library';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const PORT = 7809;
const DIST = path.join(__dirname, 'dist');

// ── Unified DW Auth Config ──
// Audit 2026-05-04: fail-closed on missing env. Old literal `DWSecure2024!`
// is now in source-control history and must be rotated.
const AUTH_USER = process.env.AUTH_USERNAME || 'admin';
const AUTH_PASS = process.env.AUTH_PASSWORD;
if (!AUTH_PASS) {
  throw new Error('AUTH_PASSWORD env var required (no fallback for production safety)');
}
const SESSION_COOKIE = 'bertha_session';
const SESSION_MAX_AGE = 30 * 24 * 60 * 60; // 30 days in seconds
const WHITELISTED_IPS = ['127.0.0.1', '::1', '45.61.58.125', 'localhost'];

// ── Google OAuth Config ──
const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID || '';
const ALLOWED_EMAILS = (process.env.ALLOWED_EMAILS || '').split(',').filter(Boolean);
const googleClient = GOOGLE_CLIENT_ID ? new OAuth2Client(GOOGLE_CLIENT_ID) : null;

const pool = new pg.Pool({
  connectionString: (process.env.DATABASE_URL || 'postgresql://dw_admin@127.0.0.1:5432/bertha_betting'),
  max: 10,
  idleTimeoutMillis: 30000,
});

const MIME = {
  '.html': 'text/html', '.js': 'application/javascript', '.css': 'text/css',
  '.json': 'application/json', '.png': 'image/png', '.svg': 'image/svg+xml',
  '.ico': 'image/x-icon', '.woff2': 'font/woff2', '.woff': 'font/woff',
};

async function q(text, params) {
  return pool.query(text, params);
}

function json(res, data, status = 200, extraHeaders = {}) {
  res.writeHead(status, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', ...extraHeaders });
  res.end(JSON.stringify(data));
}

async function readBody(req) {
  return new Promise(resolve => {
    let d = '';
    req.on('data', c => d += c);
    req.on('end', () => { try { resolve(JSON.parse(d)); } catch { resolve({}); } });
  });
}

// ── Unified Session Helpers ──
function parseCookies(req) {
  const obj = {};
  (req.headers.cookie || '').split(';').forEach(c => {
    const [k, ...v] = c.trim().split('=');
    if (k) obj[k] = v.join('=');
  });
  return obj;
}

function getSession(req) {
  const cookies = parseCookies(req);
  const token = cookies[SESSION_COOKIE];
  if (!token) return null;
  try {
    const data = JSON.parse(Buffer.from(decodeURIComponent(token), 'base64').toString());
    if (!data.authenticated) return null;
    const age = Date.now() - data.loginTime;
    if (age > SESSION_MAX_AGE * 1000) return null;
    return data;
  } catch { return null; }
}

function getClientIP(req) {
  return req.headers['x-forwarded-for']?.split(',')[0]?.trim() || req.socket.remoteAddress || '';
}

function isWhitelistedIP(req) {
  const ip = getClientIP(req);
  return WHITELISTED_IPS.some(w => ip.includes(w));
}

function hasBasicAuth(req) {
  const auth = req.headers.authorization;
  if (!auth || !auth.startsWith('Basic ')) return false;
  const [user, pass] = Buffer.from(auth.split(' ')[1], 'base64').toString().split(':');
  return user === AUTH_USER && pass === AUTH_PASS;
}

function isAuthenticated(req) {
  if (isWhitelistedIP(req)) return true;
  if (hasBasicAuth(req)) return true;
  if (getSession(req)) return true;
  return false;
}

function setSessionCookie(res, sessionToken) {
  const cookie = `${SESSION_COOKIE}=${encodeURIComponent(sessionToken)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${SESSION_MAX_AGE}`;
  return cookie;
}

function clearSessionCookie() {
  return `${SESSION_COOKIE}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`;
}

// API route handlers
const routes = {
  'GET /api/health': async (req, res) => {
    try {
      const dbCheck = await q('SELECT 1');
      const risk = await q('SELECT * FROM risk_state ORDER BY updated_at DESC LIMIT 1');
      const r = risk.rows[0] || {};
      const slack = process.env.SLACK_BOT_TOKEN ? true : false;
      json(res, {
        status: 'ok',
        db_connected: !!dbCheck,
        paper_mode: r.config?.paper_mode ?? true,
        kill_switch: r.kill_switch_active ?? false,
        slack_configured: slack,
        setup_complete: r.config?.setup_complete || false,
      });
    } catch (err) {
      json(res, { status: 'error', error: err.message }, 500);
    }
  },

  // ── Unified Auth Endpoints ──
  'POST /api/auth/login': async (req, res) => {
    const body = await readBody(req);
    if (body.username === AUTH_USER && body.password === AUTH_PASS) {
      const sessionData = { authenticated: true, username: AUTH_USER, loginTime: Date.now() };
      const token = Buffer.from(JSON.stringify(sessionData)).toString('base64');
      json(res, { success: true, message: 'Login successful', username: AUTH_USER }, 200, {
        'Set-Cookie': setSessionCookie(res, token),
      });
    } else {
      json(res, { error: 'Invalid credentials' }, 401);
    }
  },

  'POST /api/auth/logout': async (req, res) => {
    json(res, { success: true, message: 'Logged out successfully' }, 200, {
      'Set-Cookie': clearSessionCookie(),
    });
  },

  'POST /api/auth/google': async (req, res) => {
    if (!googleClient) return json(res, { error: 'Google OAuth not configured' }, 400);
    const body = await readBody(req);
    if (!body.credential) return json(res, { error: 'Missing credential' }, 400);
    try {
      const ticket = await googleClient.verifyIdToken({ idToken: body.credential, audience: GOOGLE_CLIENT_ID });
      const payload = ticket.getPayload();
      const email = payload.email;
      if (!payload.email_verified) return json(res, { error: 'Email not verified' }, 401);
      // Check allowed emails (if list configured, enforce it; otherwise allow any Google user)
      if (ALLOWED_EMAILS.length > 0 && !ALLOWED_EMAILS.includes(email)) {
        return json(res, { error: 'Email not authorized' }, 403);
      }
      const sessionData = { authenticated: true, username: payload.name || email, email, picture: payload.picture, provider: 'google', loginTime: Date.now() };
      const token = Buffer.from(JSON.stringify(sessionData)).toString('base64');
      json(res, { success: true, username: payload.name || email, email, picture: payload.picture }, 200, {
        'Set-Cookie': setSessionCookie(res, token),
      });
    } catch (err) {
      json(res, { error: 'Invalid Google token', details: err.message }, 401);
    }
  },

  'GET /api/auth/google-config': async (req, res) => {
    json(res, { client_id: GOOGLE_CLIENT_ID || null, configured: !!GOOGLE_CLIENT_ID });
  },

  'GET /api/auth/session': async (req, res) => {
    if (isWhitelistedIP(req)) {
      return json(res, { authenticated: true, username: AUTH_USER, bypassReason: 'whitelisted_ip' });
    }
    if (hasBasicAuth(req)) {
      return json(res, { authenticated: true, username: AUTH_USER, bypassReason: 'basic_auth' });
    }
    const session = getSession(req);
    if (session) {
      return json(res, { authenticated: true, username: session.username, email: session.email, picture: session.picture, provider: session.provider, loginTime: session.loginTime });
    }
    json(res, { authenticated: false }, 401);
  },

  // ── Setup Wizard ──
  'GET /api/setup': async (req, res) => {
    try {
      const r = await q('SELECT config FROM risk_state ORDER BY updated_at DESC LIMIT 1');
      const cfg = r.rows[0]?.config || {};
      json(res, { setup_complete: cfg.setup_complete || false, setup_progress: cfg.setup_progress || null });
    } catch (err) { json(res, { error: err.message }, 500); }
  },

  'POST /api/setup': async (req, res) => {
    const body = await readBody(req);
    try {
      const current = await q('SELECT * FROM risk_state ORDER BY updated_at DESC LIMIT 1');
      const row = current.rows[0] || {};
      if (body.action === 'save_progress') {
        const newConfig = { ...row.config, setup_progress: body.progress };
        await q('UPDATE risk_state SET config = $1, updated_at = NOW() WHERE id = $2', [JSON.stringify(newConfig), row.id]);
        json(res, { success: true });
      } else if (body.action === 'complete') {
        const newConfig = { ...row.config, setup_complete: true, setup_completed_at: new Date().toISOString(), setup_progress: body.progress || row.config?.setup_progress };
        await q('UPDATE risk_state SET config = $1, updated_at = NOW() WHERE id = $2', [JSON.stringify(newConfig), row.id]);
        json(res, { success: true });
      } else {
        json(res, { error: 'Unknown action' }, 400);
      }
    } catch (err) { json(res, { error: err.message }, 500); }
  },

  'GET /api/dashboard': async (req, res) => {
    try {
      const [markets, weatherMarkets, forecasts, predictions, trades, alerts, risk] = await Promise.all([
        q('SELECT COUNT(*) as cnt FROM markets'),
        q("SELECT COUNT(*) as cnt FROM markets WHERE category = 'weather' OR parsed_event IS NOT NULL"),
        q('SELECT COUNT(*) as cnt FROM forecast_runs'),
        q('SELECT * FROM predictions ORDER BY asof_ts DESC LIMIT 10'),
        q('SELECT t.*, o.side, o.outcome FROM trades t LEFT JOIN orders o ON t.order_id = o.order_id ORDER BY t.match_time DESC NULLS LAST LIMIT 10'),
        q('SELECT * FROM alerts_log ORDER BY ts DESC LIMIT 5'),
        q('SELECT * FROM risk_state ORDER BY updated_at DESC LIMIT 1'),
      ]);
      const r = risk.rows[0] || {};
      json(res, {
        market_count: parseInt(markets.rows[0]?.cnt || 0),
        weather_market_count: parseInt(weatherMarkets.rows[0]?.cnt || 0),
        forecast_count: parseInt(forecasts.rows[0]?.cnt || 0),
        recent_predictions: predictions.rows,
        recent_trades: trades.rows,
        recent_alerts: alerts.rows,
        risk: {
          bankroll: parseFloat(r.bankroll || 50),
          daily_pnl: parseFloat(r.daily_pnl || 0),
          open_exposure: parseFloat(r.open_exposure || 0),
          max_drawdown_pct: parseFloat(r.max_drawdown_pct || 0),
          paper_mode: r.config?.paper_mode ?? true,
          kill_switch_active: r.kill_switch_active || false,
          config: r.config || {},
        },
      });
    } catch (err) {
      json(res, { error: err.message }, 500);
    }
  },

  'GET /api/markets': async (req, res) => {
    try {
      const r = await q('SELECT * FROM markets ORDER BY created_at DESC LIMIT 100');
      const tokens = await q('SELECT * FROM tokens ORDER BY condition_id');
      json(res, { markets: r.rows, tokens: tokens.rows });
    } catch (err) {
      json(res, { error: err.message }, 500);
    }
  },

  'POST /api/markets': async (req, res) => {
    const body = await readBody(req);
    if (body.action === 'test_api') {
      try {
        const r = await fetch('https://gamma-api.polymarket.com/markets?limit=5&active=true');
        const data = await r.json();
        json(res, { success: true, market_count: data.length });
      } catch (err) {
        json(res, { error: err.message }, 500);
      }
    } else if (body.action === 'ingest') {
      // Forward to Next.js for complex ingest logic
      try {
        const r = await fetch('http://127.0.0.1:7800/api/markets', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(body),
        });
        json(res, await r.json());
      } catch (err) {
        json(res, { error: 'Backend unavailable: ' + err.message }, 502);
      }
    } else {
      json(res, { error: 'Unknown action' }, 400);
    }
  },

  'GET /api/weather': async (req, res) => {
    try {
      const r = await q('SELECT * FROM forecast_runs ORDER BY created_at DESC LIMIT 200');
      json(res, { forecasts: r.rows });
    } catch (err) {
      json(res, { error: err.message }, 500);
    }
  },

  'POST /api/weather': async (req, res) => {
    const body = await readBody(req);
    // Forward to Next.js for NWS API calls
    try {
      const r = await fetch('http://127.0.0.1:7800/api/weather', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(body),
      });
      json(res, await r.json());
    } catch (err) {
      json(res, { error: 'Backend unavailable: ' + err.message }, 502);
    }
  },

  'GET /api/models': async (req, res) => {
    try {
      const [models, predictions] = await Promise.all([
        q('SELECT * FROM model_versions ORDER BY created_at DESC'),
        q('SELECT p.*, m.question FROM predictions p LEFT JOIN markets m ON p.condition_id = m.condition_id ORDER BY p.asof_ts DESC LIMIT 50'),
      ]);
      json(res, { models: models.rows, predictions: predictions.rows });
    } catch (err) {
      json(res, { error: err.message }, 500);
    }
  },

  'POST /api/models': async (req, res) => {
    const body = await readBody(req);
    try {
      const r = await fetch('http://127.0.0.1:7800/api/models', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(body),
      });
      json(res, await r.json());
    } catch (err) {
      json(res, { error: 'Backend unavailable: ' + err.message }, 502);
    }
  },

  'GET /api/risk': async (req, res) => {
    try {
      const r = await q('SELECT * FROM risk_state ORDER BY updated_at DESC LIMIT 1');
      const row = r.rows[0] || {};
      json(res, {
        state: {
          bankroll: parseFloat(row.bankroll || 50),
          daily_pnl: parseFloat(row.daily_pnl || 0),
          open_exposure: parseFloat(row.open_exposure || 0),
          max_drawdown_pct: parseFloat(row.max_drawdown_pct || 0),
          kill_switch_active: row.kill_switch_active || false,
          kill_reason: row.kill_reason,
          config: row.config || {},
        },
      });
    } catch (err) {
      json(res, { error: err.message }, 500);
    }
  },

  'POST /api/risk': async (req, res) => {
    const body = await readBody(req);
    try {
      const current = await q('SELECT * FROM risk_state ORDER BY updated_at DESC LIMIT 1');
      const row = current.rows[0] || {};

      if (body.action === 'kill_switch_activate') {
        await q('UPDATE risk_state SET kill_switch_active = true, kill_reason = $1, updated_at = NOW() WHERE id = $2', [body.reason || 'Manual', row.id]);
      } else if (body.action === 'kill_switch_deactivate') {
        await q('UPDATE risk_state SET kill_switch_active = false, kill_reason = NULL, updated_at = NOW() WHERE id = $1', [row.id]);
      } else if (body.action === 'update_config') {
        const newConfig = { ...row.config, ...body.config };
        await q('UPDATE risk_state SET config = $1, updated_at = NOW() WHERE id = $2', [JSON.stringify(newConfig), row.id]);
      }

      const updated = await q('SELECT * FROM risk_state ORDER BY updated_at DESC LIMIT 1');
      const u = updated.rows[0] || {};
      json(res, {
        success: true,
        state: {
          bankroll: parseFloat(u.bankroll || 50),
          daily_pnl: parseFloat(u.daily_pnl || 0),
          open_exposure: parseFloat(u.open_exposure || 0),
          max_drawdown_pct: parseFloat(u.max_drawdown_pct || 0),
          kill_switch_active: u.kill_switch_active || false,
          kill_reason: u.kill_reason,
          config: u.config || {},
        },
      });
    } catch (err) {
      json(res, { error: err.message }, 500);
    }
  },

  'GET /api/trades': async (req, res) => {
    try {
      const [orders, trades] = await Promise.all([
        q('SELECT * FROM orders ORDER BY submitted_ts DESC LIMIT 50'),
        q('SELECT * FROM trades ORDER BY match_time DESC NULLS LAST LIMIT 50'),
      ]);
      json(res, { orders: orders.rows, trades: trades.rows });
    } catch (err) {
      json(res, { error: err.message }, 500);
    }
  },

  'POST /api/trades': async (req, res) => {
    const body = await readBody(req);
    try {
      const r = await fetch('http://127.0.0.1:7800/api/trades', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(body),
      });
      json(res, await r.json());
    } catch (err) {
      json(res, { error: 'Backend unavailable: ' + err.message }, 502);
    }
  },

  'GET /api/bankroll': async (req, res) => {
    try {
      const [ledger, summary] = await Promise.all([
        q('SELECT * FROM bankroll_ledger ORDER BY ts DESC LIMIT 200'),
        q(`SELECT
          COALESCE(SUM(CASE WHEN event_type='deposit' THEN amount ELSE 0 END), 0) as total_deposits,
          COALESCE(SUM(CASE WHEN event_type='withdrawal' THEN amount ELSE 0 END), 0) as total_withdrawals,
          COALESCE(SUM(CASE WHEN event_type='trade_pnl' THEN amount ELSE 0 END), 0) as total_pnl,
          COALESCE(SUM(CASE WHEN event_type='fee' THEN amount ELSE 0 END), 0) as total_fees,
          (SELECT balance_after FROM bankroll_ledger ORDER BY ts DESC LIMIT 1) as current_balance
        FROM bankroll_ledger`),
      ]);
      json(res, { ledger: ledger.rows, summary: summary.rows[0] || {} });
    } catch (err) {
      json(res, { error: err.message }, 500);
    }
  },

  'GET /api/alerts': async (req, res) => {
    try {
      const r = await q('SELECT * FROM alerts_log ORDER BY ts DESC LIMIT 50');
      json(res, { alerts: r.rows });
    } catch (err) {
      json(res, { error: err.message }, 500);
    }
  },

  'POST /api/alerts': async (req, res) => {
    const body = await readBody(req);
    try {
      const r = await fetch('http://127.0.0.1:7800/api/alerts', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(body),
      });
      json(res, await r.json());
    } catch (err) {
      json(res, { error: 'Backend unavailable: ' + err.message }, 502);
    }
  },

  'POST /api/workers': async (req, res) => {
    const body = await readBody(req);
    try {
      const r = await fetch('http://127.0.0.1:7800/api/workers', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(body),
      });
      json(res, await r.json());
    } catch (err) {
      json(res, { error: 'Backend unavailable: ' + err.message }, 502);
    }
  },

  // ── Polymarket CLOB API (direct HTTP) ──
  'POST /api/polymarket': async (req, res) => {
    const body = await readBody(req);
    const CLOB = 'https://clob.polymarket.com';
    const GAMMA = 'https://gamma-api.polymarket.com';

    // Helper: HMAC-SHA256 sign for L2 auth
    function hmacSign(secret, timestamp, method, path, bodyStr = '') {
      const message = timestamp + method + path + bodyStr;
      return crypto.createHmac('sha256', Buffer.from(secret, 'base64')).update(message).digest('base64');
    }

    // Helper: build L2 auth headers
    function l2Headers(apiKey, secret, passphrase, method, urlPath, bodyStr = '') {
      const ts = Math.floor(Date.now() / 1000).toString();
      const sig = hmacSign(secret, ts, method, urlPath, bodyStr);
      return { 'POLY_API_KEY': apiKey, 'POLY_PASSPHRASE': passphrase, 'POLY_TIMESTAMP': ts, 'POLY_SIGNATURE': sig };
    }

    if (body.action === 'test_public') {
      // Test public CLOB connection — no auth needed
      try {
        const [okRes, timeRes] = await Promise.all([
          fetch(`${CLOB}/ok`),
          fetch(`${CLOB}/time`),
        ]);
        const ok = await okRes.text();
        const time = await timeRes.text();
        json(res, { success: true, ok: ok.trim(), server_time: time.trim(), endpoint: CLOB });
      } catch (err) {
        json(res, { success: false, error: err.message }, 500);
      }
    } else if (body.action === 'save_keys') {
      // Save manually-entered API credentials (from polymarket.com/settings?tab=builder)
      if (!body.api_key || !body.secret || !body.passphrase) {
        return json(res, { error: 'api_key, secret, and passphrase required' }, 400);
      }
      try {
        const current = await q('SELECT * FROM risk_state ORDER BY updated_at DESC LIMIT 1');
        const row = current.rows[0] || {};
        const newConfig = {
          ...row.config,
          poly_api_key: body.api_key,
          poly_api_secret: body.secret,
          poly_passphrase: body.passphrase,
          poly_wallet: body.wallet || null,
          poly_connected: true,
          poly_connected_at: new Date().toISOString(),
        };
        await q('UPDATE risk_state SET config = $1, updated_at = NOW() WHERE id = $2', [JSON.stringify(newConfig), row.id]);
        json(res, { success: true, api_key: body.api_key.substring(0, 8) + '...', message: 'Credentials saved' });
      } catch (err) {
        json(res, { success: false, error: err.message }, 500);
      }
    } else if (body.action === 'auth_test') {
      // Test authenticated connection with stored L2 credentials
      try {
        const current = await q('SELECT config FROM risk_state ORDER BY updated_at DESC LIMIT 1');
        const cfg = current.rows[0]?.config || {};
        if (!cfg.poly_api_key || !cfg.poly_api_secret || !cfg.poly_passphrase) {
          return json(res, { error: 'No Polymarket credentials configured. Save API keys first.' }, 400);
        }
        const authHdrs = l2Headers(cfg.poly_api_key, cfg.poly_api_secret, cfg.poly_passphrase, 'GET', '/data/trades');
        const r = await fetch(`${CLOB}/data/trades`, { headers: authHdrs });
        if (r.ok) {
          const data = await r.json();
          json(res, { success: true, trades: Array.isArray(data) ? data.length : 0, wallet: cfg.poly_wallet, message: 'Authenticated OK' });
        } else {
          const errText = await r.text();
          json(res, { success: false, error: `CLOB returned ${r.status}: ${errText}` });
        }
      } catch (err) {
        json(res, { success: false, error: err.message }, 500);
      }
    } else if (body.action === 'get_markets') {
      // Get live weather markets from Gamma API
      try {
        const r = await fetch(`${GAMMA}/markets?limit=50&active=true&tag=weather`);
        const markets = await r.json();
        // Also try getting prices for our tracked tokens
        const tokens = await q('SELECT t.token_id, t.outcome, m.question FROM tokens t JOIN markets m ON t.condition_id = m.condition_id LIMIT 20');
        const prices = [];
        for (const tok of tokens.rows) {
          try {
            const pr = await fetch(`${CLOB}/price?token_id=${tok.token_id}&side=buy`);
            if (pr.ok) { const d = await pr.json(); prices.push({ ...tok, price: d.price }); }
          } catch { /* skip */ }
        }
        json(res, { success: true, gamma_markets: markets.length || 0, tracked_prices: prices });
      } catch (err) {
        json(res, { success: false, error: err.message }, 500);
      }
    } else if (body.action === 'get_orderbook') {
      if (!body.token_id) return json(res, { error: 'token_id required' }, 400);
      try {
        const r = await fetch(`${CLOB}/book?token_id=${body.token_id}`);
        const book = await r.json();
        json(res, { success: true, orderbook: book });
      } catch (err) {
        json(res, { success: false, error: err.message }, 500);
      }
    } else if (body.action === 'status') {
      // Get connection status from DB
      try {
        const current = await q('SELECT config FROM risk_state ORDER BY updated_at DESC LIMIT 1');
        const cfg = current.rows[0]?.config || {};
        json(res, {
          connected: !!cfg.poly_connected,
          api_key: cfg.poly_api_key ? cfg.poly_api_key.substring(0, 8) + '...' : null,
          wallet: cfg.poly_wallet || null,
          connected_at: cfg.poly_connected_at || null,
          has_secret: !!cfg.poly_api_secret,
          has_passphrase: !!cfg.poly_passphrase,
        });
      } catch (err) {
        json(res, { error: err.message }, 500);
      }
    } else if (body.action === 'disconnect') {
      try {
        const current = await q('SELECT * FROM risk_state ORDER BY updated_at DESC LIMIT 1');
        const row = current.rows[0] || {};
        const newConfig = { ...row.config, poly_api_key: null, poly_api_secret: null, poly_passphrase: null, poly_wallet: null, poly_connected: false, poly_connected_at: null };
        await q('UPDATE risk_state SET config = $1, updated_at = NOW() WHERE id = $2', [JSON.stringify(newConfig), row.id]);
        json(res, { success: true, message: 'Disconnected' });
      } catch (err) {
        json(res, { error: err.message }, 500);
      }
    } else {
      json(res, { error: 'Unknown action. Use: test_public, save_keys, auth_test, get_markets, get_orderbook, status, disconnect' }, 400);
    }
  },
};

const server = http.createServer(async (req, res) => {
  // CORS preflight
  if (req.method === 'OPTIONS') {
    res.writeHead(200, {
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
      'Access-Control-Allow-Headers': 'Content-Type',
    });
    res.end();
    return;
  }

  // API routes - direct DB access
  const routeKey = `${req.method} ${req.url.split('?')[0]}`;
  const urlPath = req.url.split('?')[0];

  // Public endpoints (no auth required)
  const publicPaths = ['/api/auth/login', '/api/auth/logout', '/api/auth/session', '/api/health'];
  const isPublic = publicPaths.includes(urlPath);

  // Enforce auth on all other API routes
  if (!isPublic && urlPath.startsWith('/api/') && !isAuthenticated(req)) {
    return json(res, { error: 'Authentication required' }, 401);
  }

  if (routes[routeKey]) {
    try {
      await routes[routeKey](req, res);
    } catch (err) {
      json(res, { error: err.message }, 500);
    }
    return;
  }

  // Fallback: proxy unknown API routes to Next.js backend
  if (req.url.startsWith('/api/')) {
    try {
      let body = '';
      if (req.method !== 'GET' && req.method !== 'HEAD') {
        body = await new Promise(resolve => {
          let d = '';
          req.on('data', c => d += c);
          req.on('end', () => resolve(d));
        });
      }
      const fetchOpts = {
        method: req.method,
        headers: { 'Content-Type': 'application/json', 'Cookie': req.headers.cookie || '' },
      };
      if (body && req.method !== 'GET') fetchOpts.body = body;
      const apiRes = await fetch(`http://127.0.0.1:7800${req.url}`, fetchOpts);
      const apiBody = await apiRes.text();
      res.writeHead(apiRes.status, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
      res.end(apiBody);
    } catch (err) {
      json(res, { error: err.message }, 502);
    }
    return;
  }

  // Serve static files
  let filePath = path.join(DIST, req.url === '/' ? 'index.html' : req.url);
  if (!fs.existsSync(filePath)) {
    filePath = path.join(DIST, 'index.html');
  }

  try {
    const content = fs.readFileSync(filePath);
    const ext = path.extname(filePath);
    res.writeHead(200, { 'Content-Type': MIME[ext] || 'application/octet-stream' });
    res.end(content);
  } catch {
    res.writeHead(404);
    res.end('Not found');
  }
});

server.listen(PORT, '0.0.0.0', () => {
  console.log(`[Bertha React] Dashboard running on http://0.0.0.0:${PORT}`);
  console.log(`[Bertha React] Direct DB + API proxy mode`);
});