← back to Abrams

public/terminal.html

230 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Abrams Terminal</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,500;1,400&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/static/css/panel.css" />
<style>
  body { background: #000; }
  .term-shell { padding: 80px 18px 40px; max-width: 1640px; margin: 0 auto; }
  .term-banner { background: #000; color: #c9a96e; font-family: var(--mono); font-size: 11px; letter-spacing: .25em; text-transform: uppercase; padding: 8px 14px; border: 1px solid #2a2620; display: flex; justify-content: space-between; align-items: center; margin-bottom: 14px; }
  .term-banner .live { display: inline-flex; align-items: center; gap: 8px; }
  .term-banner .live .dot { width: 7px; height: 7px; background: #4ade80; border-radius: 50%; animation: tpulse 1.4s infinite ease-in-out; }
  @keyframes tpulse { 0%,100% { opacity: 1; } 50% { opacity: .3; } }

  .term-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }
  @media (max-width: 1100px) { .term-grid { grid-template-columns: 1fr; } }

  .term-pane { background: #0a0a0a; border: 1px solid #2a2620; }
  .term-pane h2 { font-family: var(--mono); font-size: 11px; letter-spacing: .3em; text-transform: uppercase; color: #c9a96e; padding: 12px 18px; margin: 0; border-bottom: 1px solid #2a2620; display: flex; justify-content: space-between; align-items: center; }
  .term-pane h2 .meta { color: #6a6457; font-weight: 400; letter-spacing: .15em; }

  table.term { width: 100%; border-collapse: collapse; font-family: var(--mono); font-size: 12px; color: #f1ece4; }
  table.term thead th { background: #141414; color: #6a6457; font-weight: 500; padding: 8px 12px; text-align: right; font-size: 9px; letter-spacing: .25em; text-transform: uppercase; border-bottom: 1px solid #2a2620; }
  table.term thead th:first-child, table.term thead th.left { text-align: left; }
  table.term tbody td { padding: 8px 12px; text-align: right; border-bottom: 1px solid #1d1d1d; vertical-align: middle; }
  table.term tbody td:first-child { text-align: left; font-weight: 600; color: #f1ece4; }
  table.term tbody td.cat { font-size: 9px; color: #6a6457; letter-spacing: .15em; text-transform: uppercase; }
  table.term tbody td.up { color: #4ade80; }
  table.term tbody td.down { color: #ef4444; }
  table.term tbody td.flat { color: #6a6457; }
  table.term tbody tr:hover { background: rgba(201,169,110,.04); }

  /* sparkline */
  .spark { width: 100px; height: 28px; vertical-align: middle; }

  /* Abrams indexes — denser, lozenge-card style */
  .abrams-ix-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; padding: 14px; }
  @media (max-width: 600px) { .abrams-ix-grid { grid-template-columns: 1fr; } }
  .abrams-ix { background: #0d0d0d; border: 1px solid #2a2620; padding: 14px 16px; position: relative; }
  .abrams-ix .head { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 6px; }
  .abrams-ix .id { font-family: var(--mono); font-size: 10px; color: #6a6457; letter-spacing: .25em; }
  .abrams-ix .icon { font-size: 18px; line-height: 1; }
  .abrams-ix .name { font-family: var(--serif); font-size: 16px; color: #f1ece4; margin: 2px 0 8px; font-weight: 500; }
  .abrams-ix .value { font-family: var(--serif); font-size: 36px; color: #c9a96e; line-height: 1; letter-spacing: .02em; font-weight: 500; }
  .abrams-ix .unit { font-family: var(--mono); font-size: 10px; color: #6a6457; margin-left: 6px; letter-spacing: .15em; text-transform: uppercase; }
  .abrams-ix .interp { font-family: var(--mono); font-size: 9px; color: #4ade80; letter-spacing: .2em; text-transform: uppercase; margin: 8px 0 6px; }
  .abrams-ix .interp.warn { color: #f59e0b; } .abrams-ix .interp.err { color: #ef4444; }
  .abrams-ix .method { font-family: var(--sans); font-size: 11px; color: #8a8478; line-height: 1.5; margin: 8px 0 0; }
  .abrams-ix .delta { font-family: var(--mono); font-size: 10px; color: #6a6457; letter-spacing: .15em; margin-top: 4px; }
  .abrams-ix .delta.up { color: #4ade80; } .abrams-ix .delta.down { color: #ef4444; }
  .abrams-ix .spark-wrap { position: absolute; bottom: 14px; right: 16px; }
  .abrams-ix.whsi.green { border-left: 4px solid #4ade80; }
  .abrams-ix.whsi.yellow { border-left: 4px solid #f59e0b; }
  .abrams-ix.whsi.orange { border-left: 4px solid #fb923c; }
  .abrams-ix.whsi.red { border-left: 4px solid #ef4444; }

  /* controls */
  .term-controls { display: flex; gap: 12px; padding: 12px 14px; align-items: center; background: #050505; border-bottom: 1px solid #2a2620; }
  .term-controls .lbl { font-family: var(--mono); font-size: 9px; color: #6a6457; letter-spacing: .25em; text-transform: uppercase; }
  .term-controls select, .term-controls input { background: #0a0a0a; border: 1px solid #2a2620; color: #f1ece4; padding: 5px 10px; font-family: var(--mono); font-size: 11px; }
</style>
</head>
<body>

<div class="chrome">
  <a class="logo" href="/">ABRAMS<span class="dot">.</span></a>
  <div class="nav-burger" role="button" aria-label="open menu">
    <span class="bars"><span></span><span></span><span></span></span>
    <span class="label">MENU</span>
  </div>
</div>

<div class="term-shell">
  <div class="term-banner">
    <span class="live"><span class="dot"></span> ABRAMS TERMINAL · LIVE</span>
    <span id="term-clock" style="color:#f1ece4;">--:--:--</span>
    <span>MARKET <span id="market-stamp">--</span> · INDEX <span id="abrams-stamp">--</span></span>
  </div>

  <div class="term-grid">

    <!-- LEFT: Bloomberg-style market panel -->
    <div class="term-pane">
      <h2>Markets · Stocks · Bonds · Commodities · Crypto <span class="meta" id="market-count">--</span></h2>
      <div class="term-controls">
        <span class="lbl">Sort</span>
        <select id="sort-market">
          <option value="cat">Category</option>
          <option value="name">Name</option>
          <option value="changePct">% Change</option>
          <option value="price">Price</option>
        </select>
        <span class="lbl">Group</span>
        <select id="group-market">
          <option value="">none</option>
          <option value="cat" selected>by category</option>
        </select>
        <span class="lbl">Refresh</span>
        <button onclick="loadMarket()" style="background:#1d1d1d;border:1px solid #2a2620;color:#f1ece4;padding:5px 12px;font-family:var(--mono);font-size:10px;letter-spacing:.2em;cursor:pointer;">RELOAD</button>
      </div>
      <table class="term" id="market-table"></table>
    </div>

    <!-- RIGHT: Abrams proprietary indexes -->
    <div class="term-pane">
      <h2>Abrams Proprietary Indexes <span class="meta" id="abrams-count">--</span></h2>
      <div class="abrams-ix-grid" id="abrams-ix-grid"></div>
    </div>

  </div>

  <p style="color:#6a6457;font-family:var(--mono);font-size:10px;letter-spacing:.15em;margin-top:24px;text-align:center;">
    Market data via Yahoo Finance (delayed ≥15 min) · Abrams indexes refreshed every 15 min by the YOLO tick · methodology in each card
  </p>
</div>

<script src="/static/js/corner-nav.js"></script>
<script src="/static/js/gamify.js"></script>
<script>
function esc(s){ return String(s||'').replace(/[&<>"']/g, m=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[m])); }
function fmt(n, decimals = 2) {
  if (n == null || isNaN(n)) return '—';
  if (Math.abs(n) >= 1e9) return (n/1e9).toFixed(2) + 'B';
  if (Math.abs(n) >= 1e6) return (n/1e6).toFixed(2) + 'M';
  if (Math.abs(n) >= 1e3 && decimals === 0) return n.toLocaleString();
  return Number(n).toFixed(decimals);
}
function sparkSvg(arr, color = '#c9a96e', w = 100, h = 28) {
  if (!arr || arr.length < 2) return '<svg class="spark"></svg>';
  const min = Math.min(...arr), max = Math.max(...arr), range = max - min || 1;
  const pts = arr.map((v, i) => `${(i / (arr.length - 1)) * w},${h - ((v - min) / range) * (h - 4) - 2}`).join(' ');
  const last = arr[arr.length - 1], first = arr[0];
  const lineColor = last >= first ? '#4ade80' : '#ef4444';
  return `<svg class="spark" viewBox="0 0 ${w} ${h}" preserveAspectRatio="none"><polyline points="${pts}" fill="none" stroke="${lineColor}" stroke-width="1.2"/></svg>`;
}

// ── markets ──────────────────────────────────────────────────────
async function loadMarket() {
  const r = await fetch('/api/market').then(x => x.json()).catch(()=>({tickers:[]}));
  const sortBy = document.getElementById('sort-market').value;
  const groupBy = document.getElementById('group-market').value;
  let rows = (r.tickers || []).slice();
  if (sortBy === 'cat') rows.sort((a,b) => (a.cat||'').localeCompare(b.cat||'') || a.name.localeCompare(b.name));
  if (sortBy === 'name') rows.sort((a,b) => a.name.localeCompare(b.name));
  if (sortBy === 'changePct') rows.sort((a,b) => (b.changePct||0) - (a.changePct||0));
  if (sortBy === 'price') rows.sort((a,b) => (b.price||0) - (a.price||0));

  const head = '<thead><tr><th class="left">Instrument</th><th>Sym</th><th>Last</th><th>Δ</th><th>Δ%</th><th>Spark 1M</th></tr></thead>';
  let body = '<tbody>';
  let lastCat = null;
  for (const t of rows) {
    if (groupBy === 'cat' && t.cat !== lastCat) {
      body += `<tr><td colspan="6" class="cat" style="background:#0d0d0d;color:#c9a96e;letter-spacing:.3em;padding:8px 12px;font-size:9px;">${esc(t.cat)}</td></tr>`;
      lastCat = t.cat;
    }
    const dir = t.change > 0 ? 'up' : (t.change < 0 ? 'down' : 'flat');
    const sign = t.change > 0 ? '+' : '';
    const decimals = t.cat === 'Bonds' ? 3 : (t.cat === 'Crypto' ? 0 : 2);
    body += `<tr>
      <td>${esc(t.name)}</td>
      <td class="cat">${esc(t.sym)}</td>
      <td>${fmt(t.price, decimals)}</td>
      <td class="${dir}">${sign}${fmt(t.change, decimals)}</td>
      <td class="${dir}">${sign}${fmt(t.changePct, 2)}%</td>
      <td>${sparkSvg(t.sparkline)}</td>
    </tr>`;
  }
  body += '</tbody>';
  document.getElementById('market-table').innerHTML = head + body;
  document.getElementById('market-count').textContent = rows.length + ' INSTRUMENTS';
  document.getElementById('market-stamp').textContent = r.generated_at ? new Date(r.generated_at).toLocaleTimeString() : '--';
}

// ── abrams indexes ───────────────────────────────────────────────
async function loadAbramsIndexes() {
  const r = await fetch('/api/abrams-indexes').then(x => x.json()).catch(()=>({indexes:[]}));
  const grid = document.getElementById('abrams-ix-grid');
  grid.innerHTML = (r.indexes || []).map(ix => {
    const isWaffle = ix.id === 'WHSI';
    const waffleCls = isWaffle ? ` whsi ${String(ix.value).toLowerCase()}` : '';
    const interpCls = /HOT|GREEN|ACCELER|SHIPPING|FILED|INBOUND/i.test(ix.interpretation) ? '' : (/WARM|YELLOW|READY|EARLY SIGNAL|NIGHT|BALANCED/i.test(ix.interpretation) ? 'warn' : (/RED|CRITICAL|COOLING/i.test(ix.interpretation) ? 'err' : ''));
    const deltaCls = ix.delta > 0 ? 'up' : (ix.delta < 0 ? 'down' : '');
    const deltaSign = (ix.delta != null && ix.delta > 0) ? '+' : '';
    return `<div class="abrams-ix${waffleCls}">
      <div class="head">
        <span class="id">${esc(ix.id)}</span>
        <span class="icon">${ix.icon || '◌'}</span>
      </div>
      <div class="name">${esc(ix.name)}</div>
      <div><span class="value" ${ix.color ? `style="color:${esc(ix.color)};"` : ''}>${esc(String(ix.value))}</span><span class="unit">${esc(ix.unit)}</span></div>
      ${ix.delta != null ? `<div class="delta ${deltaCls}">${deltaSign}${ix.delta} · ${esc(ix.delta_label)}</div>` : `<div class="delta">${esc(ix.delta_label)}</div>`}
      <div class="interp ${interpCls}">${esc(ix.interpretation)}</div>
      <div class="method">${esc(ix.method)}</div>
      ${ix.sparkline ? `<div class="spark-wrap">${sparkSvg(ix.sparkline)}</div>` : ''}
    </div>`;
  }).join('');
  document.getElementById('abrams-count').textContent = (r.indexes||[]).length + ' INDEXES';
  document.getElementById('abrams-stamp').textContent = r.generated_at ? new Date(r.generated_at).toLocaleTimeString() : '--';
}

// ── clock ────────────────────────────────────────────────────────
function tickClock() {
  const t = new Date();
  document.getElementById('term-clock').textContent = t.toLocaleTimeString('en-US', { hour12: false });
}

// hydrate sort/group from localStorage BEFORE first load (Steve standing rule)
const savedMarketSort = localStorage.getItem('abrams.terminal.sortMarket');
if (savedMarketSort) {
  const el = document.getElementById('sort-market');
  if ([...el.options].some(o => o.value === savedMarketSort)) el.value = savedMarketSort;
}
const savedMarketGroup = localStorage.getItem('abrams.terminal.groupMarket');
if (savedMarketGroup !== null) {
  const el = document.getElementById('group-market');
  if ([...el.options].some(o => o.value === savedMarketGroup)) el.value = savedMarketGroup;
}
document.getElementById('sort-market').addEventListener('change', e => { localStorage.setItem('abrams.terminal.sortMarket', e.target.value); loadMarket(); });
document.getElementById('group-market').addEventListener('change', e => { localStorage.setItem('abrams.terminal.groupMarket', e.target.value); loadMarket(); });

loadMarket(); loadAbramsIndexes(); tickClock();
setInterval(tickClock, 1000);
setInterval(()=>{loadMarket(); loadAbramsIndexes();}, 60_000);
</script>
</body>
</html>