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