← back to Ken Trade Siren

server.js

69 lines

// ken-trade-siren — tiny local server that the desktop siren app polls.
// Serves the fullscreen alarm app and a /trades endpoint (reads ken_trades via psql).
// Independent of the watch loop — the app alarms on its own whenever ken_trades increases.
const http = require('http');
const { execFile } = require('child_process');
const fs = require('fs');
const path = require('path');
const PORT = process.env.SIREN_PORT || 7788;
const HTML = fs.readFileSync(path.join(__dirname, 'public', 'index.html'));

let testBump = 0; // each /test hit adds 1 so the app sees an "increase" and fires — no DB write

function q(sql, cb) {
  execFile('psql', ['-d', 'ken', '-tAc', sql], { timeout: 5000 }, (e, so) =>
    cb(e ? null : (so || '').trim()));
}

// ── Server-side watch loop (added 2026-08-04) ──────────────────────────────
// The browser app only alarms when a tab is open; this makes the siren fire
// (sound + CNCP card + George email) whether or not anyone is watching.
const STATE = path.join(__dirname, '.last-count');
const latestLabelSql = "SELECT COALESCE(ticker,'?')||' '||COALESCE(side,'?')||' x'||COALESCE(count::text,'?')||' @ '||COALESCE((price)::text,'?') FROM ken_trades ORDER BY 1 DESC LIMIT 1";
function fireAlert(label) {
  execFile('bash', [path.join(__dirname, 'alert-real-trade.sh'), label || 'a real Ken trade'],
    { timeout: 30000 }, (e, so) => { if (so) process.stdout.write(so); });
}
let lastCount = null;
try { const v = parseInt(fs.readFileSync(STATE, 'utf8').trim(), 10); if (!isNaN(v)) lastCount = v; } catch (_) {}
function checkTrades() {
  q('SELECT count(*) FROM ken_trades', (s) => {
    if (s === null) return;                       // DB blip — skip this tick
    const n = parseInt(s, 10); if (isNaN(n)) return;
    if (lastCount === null) { lastCount = n; try { fs.writeFileSync(STATE, String(n)); } catch (_) {} return; } // baseline, no alarm
    if (n > lastCount) {
      const prev = lastCount; lastCount = n; try { fs.writeFileSync(STATE, String(n)); } catch (_) {}
      q(latestLabelSql, (lbl) => { console.log(`[siren] REAL TRADE detected: count ${prev}->${n} — ${lbl}`); fireAlert(lbl); });
    } else if (n < lastCount) { lastCount = n; try { fs.writeFileSync(STATE, String(n)); } catch (_) {} } // resync down (truncate/reset)
  });
}
setInterval(checkTrades, 20000);
setTimeout(checkTrades, 2000); // prime shortly after boot (sets baseline)
http.createServer((req, res) => {
  if (req.url.startsWith('/test')) {
    testBump++;
    fireAlert('🚨 TEST SIREN — not a real trade'); // exercise the full server-side path (sound+CNCP+email)
    res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
    res.end(JSON.stringify({ ok: true, testBump, serverAlert: true }));
    return;
  }
  if (req.url.startsWith('/trades')) {
    q('SELECT count(*) FROM ken_trades', (cnt) => {
      q("SELECT COALESCE(ticker,'?')||' '||COALESCE(side,'?')||' x'||COALESCE(count::text,'?')||' @ '||COALESCE((price)::text,'?') FROM ken_trades ORDER BY 1 DESC LIMIT 1", (latest) => {
        // also read the switch so the app can show ARMED / SAFE
        execFile('psql', ['-d', 'bertha_betting', '-tAc', "SELECT config->>'trading_on'||'/'||(config->>'safe_mode') FROM risk_state ORDER BY updated_at DESC LIMIT 1"], { timeout: 5000 }, (e, so) => {
          const gate = e ? '?/?' : (so || '').trim();
          const real = cnt === null ? null : parseInt(cnt, 10);
          const trades = real === null ? null : real + testBump;
          const lbl = testBump > 0 ? '🚨 TEST SIREN — not a real trade' : (latest || '');
          res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-store' });
          res.end(JSON.stringify({ trades, latest: lbl, gate }));
        });
      });
    });
    return;
  }
  res.writeHead(200, { 'Content-Type': 'text/html' });
  res.end(HTML);
}).listen(PORT, () => console.log('ken-trade-siren on http://127.0.0.1:' + PORT));