[object Object]

← back to Ken Trade Siren

Add server-side watch loop: siren fires (sound + CNCP + George email) on any new real trade, browser tab or not

413b696da5ad3d6c7ac9dad419dad8d011181fd0 · 2026-08-04 11:07:50 -0700 · Steve Abrams

Files touched

Diff

commit 413b696da5ad3d6c7ac9dad419dad8d011181fd0
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Aug 4 11:07:50 2026 -0700

    Add server-side watch loop: siren fires (sound + CNCP + George email) on any new real trade, browser tab or not
---
 .gitignore          |   4 ++
 alert-real-trade.sh |  34 ++++++++++
 public/index.html   | 188 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 server.js           |  68 +++++++++++++++++++
 4 files changed, 294 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..b6537be
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+.last-count
+node_modules/
+*.log
+.DS_Store
diff --git a/alert-real-trade.sh b/alert-real-trade.sh
new file mode 100755
index 0000000..f0fd7f1
--- /dev/null
+++ b/alert-real-trade.sh
@@ -0,0 +1,34 @@
+#!/bin/bash
+# Fired by the siren's server-side watch loop when a NEW real Ken trade lands.
+# Three channels so a real-money buy is never silent, browser tab open or not:
+#   1) local sound (afplay + say) — works with no browser
+#   2) CNCP parking-lot card
+#   3) George email to steve-office
+# Arg $1 = human label of the latest trade (e.g. "KXHIGHNY... yes x1 @ 0.76").
+LABEL="${1:-a real Ken trade}"
+CNCP="${CNCP:-http://127.0.0.1:3333}"
+TS="$(date '+%Y-%m-%d %H:%M:%S %Z')"
+
+# 1) LOCAL SOUND — non-blocking, best-effort
+( afplay /System/Library/Sounds/Sosumi.aiff 2>/dev/null; \
+  afplay /System/Library/Sounds/Sosumi.aiff 2>/dev/null; \
+  say -r 190 "Ken just placed a real money trade. $LABEL" 2>/dev/null ) &
+
+# 2) CNCP parking-lot card
+curl -sS --max-time 10 "$CNCP/api/parking-lot" -H 'Content-Type: application/json' \
+  -d "{\"url\":\"ken://real-trade\",\"note\":\"[KEN SIREN] Real-money trade placed at $TS — $LABEL. Ken is LIVE (real Kalshi order on the books).\"}" \
+  >/dev/null 2>&1 && echo "[siren] CNCP card posted" || echo "[siren] CNCP post failed"
+
+# 3) George email (steve-office)
+if [ -f "$HOME/.claude/skills/_shared/george-send.sh" ]; then
+  . "$HOME/.claude/skills/_shared/george-send.sh"
+  HTML="<h2 style=\"color:#b23b3b\">🚨 Ken placed a REAL-money trade</h2>\
+<div><b>${LABEL}</b></div><div style=\"color:#666\">${TS}</div>\
+<div style=\"margin-top:10px\">Ken has a real Kalshi order on the books. \
+Dashboard: <a href=\"http://127.0.0.1:7810\">127.0.0.1:7810</a> \
+(real-money view: <a href=\"http://127.0.0.1:7810/?real=1\">/?real=1</a>).</div>"
+  george_send steve-office steve@designerwallcoverings.com "🚨 Ken real-money trade placed — ${LABEL}" "$HTML" \
+    >/dev/null 2>&1 && echo "[siren] George email sent" || echo "[siren] George email failed"
+else
+  echo "[siren] george-send.sh not found — email skipped"
+fi
diff --git a/public/index.html b/public/index.html
new file mode 100644
index 0000000..8c00454
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,188 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>🚨 KEN TRADE SIREN</title>
+<style>
+  :root { --bg:#05060a; }
+  * { box-sizing:border-box; margin:0; padding:0; }
+  html,body { height:100%; background:var(--bg); overflow:hidden;
+    font-family:-apple-system,BlinkMacSystemFont,"SF Pro Display",system-ui,sans-serif; color:#e8ecf4; }
+  #stage { position:fixed; inset:0; display:flex; align-items:center; justify-content:center; }
+
+  /* ---- IDLE (armed, watching) ---- */
+  #idle { text-align:center; z-index:5; transition:opacity .4s; }
+  #idle .title { font-size:clamp(28px,6vw,72px); font-weight:800; letter-spacing:.06em;
+    background:linear-gradient(90deg,#5aa9ff,#8affc1); -webkit-background-clip:text; background-clip:text; color:transparent; }
+  #idle .beacon { font-size:clamp(60px,14vw,150px); animation:idlepulse 2.2s ease-in-out infinite; }
+  #idle .sub { margin-top:14px; font-size:clamp(14px,2.2vw,22px); color:#9fb0c8; letter-spacing:.04em; }
+  #idle .gate { margin-top:6px; font-size:14px; color:#6b7a93; font-variant-numeric:tabular-nums; }
+  @keyframes idlepulse { 0%,100%{ transform:scale(1); opacity:.85 } 50%{ transform:scale(1.08); opacity:1 } }
+
+  /* ---- ALARM ---- */
+  #alarm { position:fixed; inset:0; display:none; z-index:20; overflow:hidden; }
+  #alarm.on { display:block; }
+  /* red/blue strobe background */
+  #strobe { position:absolute; inset:0; animation:strobe .28s steps(1) infinite; }
+  @keyframes strobe {
+    0%   { background:radial-gradient(circle at 30% 40%, #ff1e2d55, #05060a 60%); box-shadow:inset 0 0 300px #ff1e2daa; }
+    50%  { background:radial-gradient(circle at 70% 60%, #1e6bff55, #05060a 60%); box-shadow:inset 0 0 300px #1e6bffaa; }
+    100% { background:radial-gradient(circle at 30% 40%, #ff1e2d55, #05060a 60%); box-shadow:inset 0 0 300px #ff1e2daa; }
+  }
+  /* rotating beacon light sweeps */
+  .sweep { position:absolute; top:50%; left:50%; width:220vmax; height:80px; transform-origin:left center;
+    background:linear-gradient(90deg, rgba(255,60,60,.55), transparent 70%); filter:blur(6px); }
+  .sweep.b2 { background:linear-gradient(90deg, rgba(60,120,255,.55), transparent 70%); }
+  .sweep.r { animation:spin 1.4s linear infinite; }
+  .sweep.r2 { animation:spin 1.4s linear infinite; animation-delay:-.7s; }
+  @keyframes spin { to { transform:rotate(360deg); } }
+
+  /* rotating emergency beacon puck */
+  #beacon { position:absolute; top:50%; left:50%; width:min(46vmin,420px); height:min(46vmin,420px);
+    transform:translate(-50%,-50%); border-radius:50%;
+    background:conic-gradient(#ff2233 0 20deg, #2a0006 20deg 180deg, #1e6bff 180deg 200deg, #00082a 200deg 360deg);
+    box-shadow:0 0 80px 20px #ff113355, 0 0 120px 40px #1e6bff33; animation:spin 1.1s linear infinite; opacity:.9; }
+  #beaconCore { position:absolute; top:50%; left:50%; width:34%; height:34%; transform:translate(-50%,-50%);
+    border-radius:50%; background:#0a0c16; display:flex; align-items:center; justify-content:center; font-size:min(16vmin,150px);
+    animation:shake .18s infinite; }
+
+  /* headline banner that slides across */
+  #banner { position:absolute; top:8%; left:0; right:0; text-align:center; z-index:22; }
+  #banner .big { display:inline-block; font-size:clamp(34px,8vw,110px); font-weight:900; letter-spacing:.04em;
+    color:#fff; text-shadow:0 0 24px #ff2a2a, 0 0 48px #ff2a2a; animation:shake .2s infinite; }
+  #detail { position:absolute; bottom:12%; left:0; right:0; text-align:center; z-index:22;
+    font-size:clamp(20px,4vw,48px); font-weight:800; color:#ffe9a8; letter-spacing:.03em;
+    text-shadow:0 0 18px #000; font-variant-numeric:tabular-nums; }
+  @keyframes shake { 0%{transform:translate(-50%,-50%) rotate(-1.5deg)} 25%{transform:translate(-51%,-49%) rotate(1.5deg)}
+    50%{transform:translate(-49%,-51%) rotate(-1deg)} 75%{transform:translate(-50%,-50%) rotate(1deg)} 100%{transform:translate(-50%,-50%)} }
+  #banner .big { animation:bshake .2s infinite; }
+  @keyframes bshake { 0%{transform:translateX(-6px) rotate(-1deg)} 50%{transform:translateX(6px) rotate(1deg)} 100%{transform:translateX(-6px)} }
+
+  /* click-to-arm overlay (unlocks audio) */
+  #arm { position:fixed; inset:0; z-index:100; background:#05060af5; display:flex; flex-direction:column;
+    align-items:center; justify-content:center; text-align:center; cursor:pointer; }
+  #arm h1 { font-size:clamp(30px,7vw,84px); font-weight:900;
+    background:linear-gradient(90deg,#ff5a5a,#5aa9ff); -webkit-background-clip:text; background-clip:text; color:transparent; }
+  #arm p { margin-top:16px; font-size:clamp(16px,2.6vw,26px); color:#9fb0c8; }
+  #arm .k { font-size:clamp(70px,16vw,180px); animation:idlepulse 1.6s ease-in-out infinite; }
+
+  #test { position:fixed; bottom:16px; right:18px; z-index:60; background:#141824; color:#9fb0c8;
+    border:1px solid #2a3242; padding:8px 14px; border-radius:9px; font-size:13px; cursor:pointer; letter-spacing:.03em; }
+  #test:hover { color:#fff; border-color:#5aa9ff; }
+  #status { position:fixed; bottom:16px; left:18px; z-index:60; font-size:13px; color:#6b7a93; font-variant-numeric:tabular-nums; }
+</style>
+</head>
+<body>
+<div id="stage">
+  <div id="idle">
+    <div class="beacon">🚨</div>
+    <div class="title">KEN TRADE SIREN</div>
+    <div class="sub" id="idleSub">ARMED — watching for real trades…</div>
+    <div class="gate" id="idleGate">trades: — · gate —</div>
+  </div>
+</div>
+
+<div id="alarm">
+  <div id="strobe"></div>
+  <div class="sweep b1 r"></div>
+  <div class="sweep b2 r2"></div>
+  <div id="beacon"></div>
+  <div id="beaconCore">🚨</div>
+  <div id="banner"><span class="big">🚨 NEW TRADE BY KEN 🚨</span></div>
+  <div id="detail"></div>
+</div>
+
+<div id="arm">
+  <div class="k">🚨</div>
+  <h1>KEN TRADE SIREN</h1>
+  <p>Click anywhere to ARM the alarm (enables sound)</p>
+</div>
+
+<button id="test">▶ test siren</button>
+<div id="status">connecting…</div>
+
+<script>
+let audioCtx = null, armed = false, lastCount = null, alarming = false;
+
+document.getElementById('arm').addEventListener('click', () => {
+  audioCtx = new (window.AudioContext || window.webkitAudioContext)();
+  armed = true;
+  document.getElementById('arm').style.display = 'none';
+});
+document.getElementById('test').addEventListener('click', (e) => {
+  e.stopPropagation();
+  if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
+  fireAlarm('TEST · SFO-DEMO yes x1 @ 42¢', 9);
+});
+
+// ---- WAILING SIREN (Web Audio, no file) ----
+function playSiren(dur) {
+  if (!audioCtx) return;
+  const t0 = audioCtx.currentTime;
+  const osc = audioCtx.createOscillator();
+  const osc2 = audioCtx.createOscillator();
+  const gain = audioCtx.createGain();
+  osc.type = 'sawtooth'; osc2.type = 'square';
+  osc.connect(gain); osc2.connect(gain); gain.connect(audioCtx.destination);
+  gain.gain.setValueAtTime(0.0001, t0);
+  gain.gain.exponentialRampToValueAtTime(0.22, t0 + 0.12);
+  const period = 0.72;
+  let t = t0;
+  for (let i = 0; i < Math.ceil(dur / period); i++) {
+    osc.frequency.setValueAtTime(520, t);
+    osc.frequency.linearRampToValueAtTime(1280, t + period / 2);
+    osc.frequency.linearRampToValueAtTime(520, t + period);
+    osc2.frequency.setValueAtTime(260, t);
+    osc2.frequency.linearRampToValueAtTime(640, t + period / 2);
+    osc2.frequency.linearRampToValueAtTime(260, t + period);
+    t += period;
+  }
+  gain.gain.setValueAtTime(0.22, t0 + dur - 0.3);
+  gain.gain.exponentialRampToValueAtTime(0.0001, t0 + dur);
+  osc.start(t0); osc2.start(t0); osc.stop(t0 + dur); osc2.stop(t0 + dur);
+}
+
+function fireAlarm(detail, dur) {
+  if (alarming) return;
+  alarming = true;
+  document.getElementById('detail').textContent = detail || '';
+  document.getElementById('alarm').classList.add('on');
+  document.getElementById('idle').style.opacity = '0';
+  try { playSiren(dur || 12); } catch (e) {}
+  // desktop notification too
+  try { if (Notification && Notification.permission === 'granted') new Notification('🚨 NEW TRADE BY KEN', { body: detail || '' }); } catch (e) {}
+  setTimeout(() => {
+    document.getElementById('alarm').classList.remove('on');
+    document.getElementById('idle').style.opacity = '1';
+    alarming = false;
+  }, (dur || 12) * 1000);
+}
+
+// ---- POLL ----
+async function poll() {
+  try {
+    const r = await fetch('/trades', { cache: 'no-store' });
+    const d = await r.json();
+    document.getElementById('status').textContent =
+      'ken:127.0.0.1:7810 · trades ' + (d.trades ?? '?') + ' · gate ' + (d.gate || '?') + ' · ' + new Date().toLocaleTimeString();
+    document.getElementById('idleGate').textContent = 'trades: ' + (d.trades ?? '—') + ' · gate ' + (d.gate || '—');
+    document.getElementById('idleSub').textContent = (d.gate && d.gate.startsWith('true'))
+      ? 'ARMED — watching for real trades…' : 'trading OFF (safe) — will still alarm on any fill';
+    if (d.trades !== null) {
+      if (lastCount === null) lastCount = d.trades;         // baseline on first read
+      else if (d.trades > lastCount) {                       // NEW TRADE!
+        const n = d.trades - lastCount; lastCount = d.trades;
+        fireAlarm((d.latest || 'new fill') + (n > 1 ? '  (+' + n + ')' : ''), 13);
+      }
+    }
+  } catch (e) {
+    document.getElementById('status').textContent = 'waiting for server…';
+  }
+}
+try { if (Notification && Notification.requestPermission) Notification.requestPermission(); } catch (e) {}
+setInterval(poll, 2000);
+poll();
+</script>
+</body>
+</html>
diff --git a/server.js b/server.js
new file mode 100644
index 0000000..f155893
--- /dev/null
+++ b/server.js
@@ -0,0 +1,68 @@
+// 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));

(oldest)  ·  back to Ken Trade Siren  ·  (newest)