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