← 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
A .gitignoreA alert-real-trade.shA public/index.htmlA server.js
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)