← back to Ken Trade Siren
public/index.html
189 lines
<!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>