<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FLEET OPS — CCSite Mission Control</title>
<style>
  * { margin:0; padding:0; box-sizing:border-box; }
  :root {
    --bg:#07090f; --panel:#0d1117; --panel2:#11161f; --line:#1c2432;
    --txt:#e6edf6; --muted:#7d8aa0; --dim:#4a5668;
    --green:#2ea043; --green2:#3fb950; --red:#f85149; --amber:#d29922;
    --cyan:#39c5cf; --purple:#a371f7; --pink:#f778ba; --orange:#ff9f43;
  }
  body { background:var(--bg); color:var(--txt); font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif; padding:18px 22px 60px; }
  .mono { font-family:'SF Mono',Menlo,monospace; }

  /* ── header ── */
  header { display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:10px; margin-bottom:18px; }
  .brand { display:flex; align-items:center; gap:12px; }
  .brand .logo { width:38px;height:38px;border-radius:10px;background:linear-gradient(135deg,#1f6feb,#a371f7);display:flex;align-items:center;justify-content:center;font-size:20px; }
  .brand h1 { font-size:1.25rem; letter-spacing:2px; font-weight:800; }
  .brand h1 span { color:var(--cyan); }
  .brand .sub { font-size:0.68rem; color:var(--muted); letter-spacing:1.5px; text-transform:uppercase; }
  .hdr-right { display:flex; align-items:center; gap:14px; font-size:0.75rem; color:var(--muted); }
  .pulse { width:8px;height:8px;border-radius:50%;background:var(--green2);display:inline-block;margin-right:6px;animation:pl 2s infinite; }
  @keyframes pl { 0%,100%{box-shadow:0 0 0 0 rgba(63,185,80,.5)} 50%{box-shadow:0 0 0 6px rgba(63,185,80,0)} }
  .nav { display:flex; gap:8px; margin-left:10px; }
  .nav a { font-size:0.7rem; color:var(--muted); text-decoration:none; padding:4px 10px; border:1px solid var(--line); border-radius:6px; }
  .nav a:hover { color:var(--txt); border-color:var(--cyan); }

  /* ── KPI strip ── */
  .kpis { display:grid; grid-template-columns:repeat(auto-fit,minmax(140px,1fr)); gap:10px; margin-bottom:14px; }
  .kpi { background:var(--panel); border:1px solid var(--line); border-radius:12px; padding:12px 14px; position:relative; overflow:hidden; }
  .kpi::after { content:''; position:absolute; top:0; left:0; right:0; height:2px; background:var(--kc,var(--cyan)); opacity:.7; }
  .kpi .l { font-size:0.62rem; color:var(--muted); text-transform:uppercase; letter-spacing:1.2px; margin-bottom:5px; }
  .kpi .v { font-size:1.45rem; font-weight:800; font-variant-numeric:tabular-nums; }
  .kpi .d { font-size:0.66rem; color:var(--dim); margin-top:3px; }
  .kpi .d b { color:var(--green2); font-weight:600; }

  /* ── charts row ── */
  .charts { display:grid; grid-template-columns:2fr 1.4fr 1fr 0.9fr; gap:10px; margin-bottom:14px; }
  .panel { background:var(--panel); border:1px solid var(--line); border-radius:12px; padding:12px 14px; }
  .panel h3 { font-size:0.68rem; color:var(--muted); text-transform:uppercase; letter-spacing:1.4px; margin-bottom:10px; display:flex; justify-content:space-between; }
  .panel h3 .tag { color:var(--dim); font-weight:400; letter-spacing:0; text-transform:none; }
  canvas { display:block; width:100%; }

  /* ── main grid ── */
  .main { display:grid; grid-template-columns:1fr 340px; gap:12px; margin-bottom:14px; }
  .sites { display:grid; grid-template-columns:repeat(auto-fill,minmax(220px,1fr)); gap:10px; }
  .site { background:var(--panel2); border:1px solid var(--line); border-radius:12px; padding:12px; transition:border-color .2s; position:relative; }
  .site:hover { border-color:#2f3d52; }
  .site.off { opacity:.55; }
  .site .hd { display:flex; align-items:center; gap:8px; margin-bottom:9px; }
  .site .em { font-size:1.3rem; }
  .site .nm { font-weight:700; font-size:0.9rem; }
  .site .ip { font-size:0.62rem; color:var(--dim); font-family:Menlo,monospace; }
  .dot { width:8px;height:8px;border-radius:50%;margin-left:auto; }
  .dot.on { background:var(--green2); box-shadow:0 0 8px var(--green2); }
  .dot.off { background:var(--red); box-shadow:0 0 8px var(--red); }
  .site .nums { display:grid; grid-template-columns:1fr 1fr 1fr; gap:6px; text-align:center; }
  .site .nums .n { font-size:0.95rem; font-weight:700; font-variant-numeric:tabular-nums; }
  .site .nums .t { font-size:0.56rem; color:var(--dim); text-transform:uppercase; letter-spacing:.8px; }
  .site .ft { display:flex; justify-content:space-between; align-items:center; margin-top:9px; font-size:0.64rem; color:var(--muted); }
  .badge-pend { background:rgba(210,153,34,.15); color:var(--amber); border:1px solid rgba(210,153,34,.35); padding:1px 7px; border-radius:10px; font-size:0.62rem; font-weight:600; }
  .badge-none { color:var(--dim); font-size:0.62rem; }

  /* ── right rail feeds ── */
  .rail { display:flex; flex-direction:column; gap:12px; }
  .feed { max-height:300px; overflow-y:auto; scrollbar-width:thin; scrollbar-color:var(--line) transparent; }
  .fi { display:flex; gap:9px; padding:7px 2px; border-bottom:1px solid var(--line); font-size:0.74rem; align-items:baseline; }
  .fi:last-child { border-bottom:none; }
  .fi .ic { flex-shrink:0; }
  .fi .tt { color:var(--dim); font-size:0.62rem; margin-left:auto; flex-shrink:0; font-family:Menlo,monospace; }
  .fi .amt { color:var(--green2); font-weight:700; font-variant-numeric:tabular-nums; }
  .fi .st-p { color:var(--amber); } .fi .st-d { color:var(--green2); }
  .fi .mb { font-size:0.6rem; padding:1px 6px; border-radius:8px; background:rgba(163,113,247,.15); color:var(--purple); border:1px solid rgba(163,113,247,.3); }

  /* ── bottom tables ── */
  .bottom { display:grid; grid-template-columns:1.6fr 1fr 1fr; gap:12px; }
  table { width:100%; border-collapse:collapse; font-size:0.74rem; }
  th { text-align:left; color:var(--dim); font-size:0.6rem; text-transform:uppercase; letter-spacing:1px; padding:5px 8px; border-bottom:1px solid var(--line); }
  td { padding:6px 8px; border-bottom:1px solid var(--line); font-variant-numeric:tabular-nums; }
  tr:last-child td { border-bottom:none; }
  td.num, th.num { text-align:right; }
  .rank { color:var(--dim); font-family:Menlo,monospace; width:22px; }
  .bar { height:4px; border-radius:2px; background:linear-gradient(90deg,var(--cyan),var(--purple)); margin-top:4px; }
  .funnel { display:flex; align-items:center; gap:6px; flex-wrap:wrap; }
  .fstep { flex:1; min-width:90px; text-align:center; background:var(--panel2); border:1px solid var(--line); border-radius:10px; padding:10px 6px; }
  .fstep .n { font-size:1.2rem; font-weight:800; }
  .fstep .l { font-size:0.58rem; color:var(--muted); text-transform:uppercase; letter-spacing:1px; margin-top:2px; }
  .farrow { color:var(--dim); font-size:1rem; }
  .stores { display:flex; flex-wrap:wrap; gap:6px; }
  .store-chip { font-size:0.64rem; padding:3px 9px; border-radius:8px; border:1px solid var(--line); background:var(--panel2); }
  .store-chip b { color:var(--green2); }
  .store-chip .nw { color:var(--red); }
  .empty { color:var(--dim); font-size:0.72rem; padding:14px 4px; text-align:center; }

  @media (max-width:1100px) { .charts { grid-template-columns:1fr 1fr; } .main { grid-template-columns:1fr; } .bottom { grid-template-columns:1fr; } }
</style>
</head>
<body>

<header>
  <div class="brand">
    <div class="logo">🛰️</div>
    <div>
      <h1>FLEET <span>OPS</span></h1>
      <div class="sub">CCSite Mission Control</div>
    </div>
    <div class="nav">
      <a href="/master">Master</a><a href="/btcpay">BTCPay</a><a href="/watchdog">Watchdog</a><a href="/recon">Recon</a>
    </div>
  </div>
  <div class="hdr-right">
    <span><span class="pulse"></span>live · poll <span id="poll-age">—</span></span>
    <span class="mono" id="clock">—</span>
  </div>
</header>

<div class="kpis" id="kpis"></div>

<div class="charts">
  <div class="panel"><h3>Revenue — fleet history <span class="tag" id="rev-total"></span></h3><canvas id="ch-rev" height="150"></canvas></div>
  <div class="panel"><h3>Activity — 14 days <span class="tag">orders vs signups</span></h3><canvas id="ch-act" height="150"></canvas></div>
  <div class="panel"><h3>Users by site</h3><canvas id="ch-users" height="150"></canvas></div>
  <div class="panel"><h3>BTC node</h3><canvas id="ch-btc" height="150"></canvas><div id="btc-sub" style="font-size:.64rem;color:var(--muted);text-align:center;margin-top:4px"></div></div>
</div>

<div class="main">
  <div>
    <div class="panel" style="margin-bottom:12px;"><h3>Fleet — 10 storefronts <span class="tag" id="fleet-tag"></span></h3>
      <div class="sites" id="sites"></div>
    </div>
    <div class="panel"><h3>Conversion funnel</h3><div class="funnel" id="funnel"></div></div>
  </div>
  <div class="rail">
    <div class="panel"><h3>💸 Live orders <span class="tag" id="ord-count"></span></h3><div class="feed" id="orders"></div></div>
    <div class="panel"><h3>👤 Signups <span class="tag" id="usr-count"></span></h3><div class="feed" id="signups"></div></div>
    <div class="panel"><h3>⚡ Events</h3><div class="feed" id="events"></div></div>
  </div>
</div>

<div class="bottom">
  <div class="panel"><h3>🏆 Site leaderboard — by revenue</h3><table id="board"></table></div>
  <div class="panel"><h3>🔥 Top products</h3><table id="top-prod"></table></div>
  <div class="panel"><h3>₿ BTCPay stores <span class="tag" id="btcpay-tag"></span></h3>
    <div class="stores" id="stores"></div>
    <h3 style="margin-top:14px;">Recent chain activity</h3>
    <div id="btc-recent" style="font-size:.7rem"></div>
  </div>
</div>

<script>
const $ = id => document.getElementById(id);
const money = n => '$' + Number(n||0).toLocaleString(undefined,{minimumFractionDigits:2,maximumFractionDigits:2});
const ago = iso => {
  if (!iso) return '—';
  const t = (Date.now() - new Date(iso.replace('T',' ').replace(/\+.*/,'') + (iso.includes('Z')||iso.includes('+')?'':' UTC'))/1000)|0;
  // ISO dates from shops are naive UTC
  const s = isNaN(t) || t < 0 ? 0 : t;
  if (s < 60) return s + 's'; if (s < 3600) return (s/60|0) + 'm';
  if (s < 86400) return (s/3600|0) + 'h'; return (s/86400|0) + 'd';
};
const agoTs = ts => ago(new Date(ts*1000).toISOString());

setInterval(() => { $('clock').textContent = new Date().toLocaleTimeString(); }, 1000);

/* ── tiny canvas chart kit ── */
function setup(cv){ const dpr = devicePixelRatio||1, r = cv.getBoundingClientRect();
  cv.width = r.width*dpr; cv.height = r.height*dpr;
  const x = cv.getContext('2d'); x.scale(dpr,dpr); return [x, r.width, r.height]; }
function area(cv, pts, color){
  if(!pts.length){ return; } const [x,W,H] = setup(cv), P=8;
  const max = Math.max(...pts.map(p=>p.v), 1);
  const bw = (W-2*P)/Math.max(pts.length-1,1);
  x.clearRect(0,0,W,H);
  const grad = x.createLinearGradient(0,0,0,H);
  grad.addColorStop(0, color+'55'); grad.addColorStop(1, color+'00');
  x.beginPath(); x.moveTo(P, H-P);
  pts.forEach((p,i)=> x.lineTo(P+i*bw, H-P-(p.v/max)*(H-2*P-14)));
  x.lineTo(P+(pts.length-1)*bw, H-P); x.closePath(); x.fillStyle = grad; x.fill();
  x.beginPath(); pts.forEach((p,i)=> i? x.lineTo(P+i*bw, H-P-(p.v/max)*(H-2*P-14)) : x.moveTo(P, H-P-(p.v/max)*(H-2*P-14)));
  x.strokeStyle = color; x.lineWidth = 1.8; x.stroke();
  const lp = pts[pts.length-1];
  x.fillStyle = color; x.beginPath(); x.arc(P+(pts.length-1)*bw, H-P-(lp.v/max)*(H-2*P-14), 3, 0, 7); x.fill();
  x.fillStyle = '#7d8aa0'; x.font = '10px Menlo'; x.fillText(money(lp.v), P+4, 12);
}
function bars(cv, labels, seriesA, seriesB){
  const [x,W,H] = setup(cv), P=8, n = labels.length; if(!n) return;
  const max = Math.max(...seriesA, ...seriesB, 1), bw = (W-2*P)/n;
  x.clearRect(0,0,W,H);
  labels.forEach((l,i)=>{
    const a = seriesA[i], b = seriesB[i];
    x.fillStyle = '#39c5cf';
    const ha = (a/max)*(H-2*P-12); x.fillRect(P+i*bw+1, H-P-ha, bw*0.55, ha);
    x.fillStyle = '#a371f7';
    const hb = (b/max)*(H-2*P-12); x.fillRect(P+i*bw+bw*0.58, H-P-hb, bw*0.35, hb);
    if(i%2===0){ x.fillStyle='#4a5668'; x.font='8px Menlo'; x.fillText(l.slice(5), P+i*bw, H-1); }
  });
}
function donut(cv, items){
  const [x,W,H] = setup(cv); x.clearRect(0,0,W,H);
  const total = items.reduce((s,i)=>s+i.v,0); if(!total){ x.fillStyle='#4a5668'; x.font='11px Menlo'; x.fillText('no users yet', 10, H/2); return; }
  const cols = ['#39c5cf','#a371f7','#f778ba','#ff9f43','#3fb950','#d29922','#f85149','#58a6ff','#7ee787','#e3b341'];
  const cx = W*0.32, cy = H/2, r = Math.min(W,H)/2 - 10; let a = -Math.PI/2;
  items.forEach((it,i)=>{ const sw = it.v/total*2*Math.PI;
    x.beginPath(); x.arc(cx,cy,r,a,a+sw); x.strokeStyle = cols[i%cols.length]; x.lineWidth = 13; x.stroke(); a += sw; });
  x.fillStyle = '#e6edf6'; x.font = 'bold 15px Menlo'; x.textAlign='center'; x.fillText(total, cx, cy+4); x.textAlign='left';
  items.slice(0,6).forEach((it,i)=>{ x.fillStyle = cols[i%cols.length]; x.fillRect(W*0.58, 14+i*20, 8, 8);
    x.fillStyle = '#7d8aa0'; x.font = '9.5px Menlo'; x.fillText(it.k+' ('+it.v+')', W*0.58+13, 21+i*20); });
}
function gauge(cv, pct, label){
  const [x,W,H] = setup(cv); x.clearRect(0,0,W,H);
  const cx = W/2, cy = H*0.62, r = Math.min(W,H)*0.42;
  x.lineWidth = 11; x.lineCap = 'round';
  x.beginPath(); x.arc(cx,cy,r,Math.PI,2*Math.PI); x.strokeStyle = '#1c2432'; x.stroke();
  x.beginPath(); x.arc(cx,cy,r,Math.PI,Math.PI+Math.PI*Math.min(pct,100)/100);
  x.strokeStyle = pct>=100 ? '#3fb950' : '#d29922'; x.stroke();
  x.fillStyle = '#e6edf6'; x.font = 'bold 17px Menlo'; x.textAlign='center';
  x.fillText(pct.toFixed(0)+'%', cx, cy-6);
  x.fillStyle = '#7d8aa0'; x.font = '9px Menlo'; x.fillText(label, cx, cy+12); x.textAlign='left';
}

/* ── renderers ── */
async function refresh(){
  const [state, orders, signups, board, top, act, fun] = await Promise.all([
    fetch('/api/state').then(r=>r.json()), fetch('/api/orders?limit=30').then(r=>r.json()),
    fetch('/api/signups?limit=30').then(r=>r.json()), fetch('/api/leaderboard').then(r=>r.json()),
    fetch('/api/top-products').then(r=>r.json()), fetch('/api/activity').then(r=>r.json()),
    fetch('/api/funnel').then(r=>r.json()),
  ]);
  const t = state.totals||{}, sites = Object.values(state.sites||{});
  const revToday = sites.reduce((s,x)=>s+(x.revenue_today||0),0);
  const ordToday = sites.reduce((s,x)=>s+(x.orders_today||0),0);
  const sgnToday = sites.reduce((s,x)=>s+(x.signups_today||0),0);

  /* KPIs */
  const k = [
    ['Fleet Revenue', money(fun.revenue), `+${money(revToday)} today`, 'var(--green2)'],
    ['Orders', fun.orders, `+${ordToday} today · AOV ${money(fun.aov)}`, 'var(--cyan)'],
    ['Pending Delivery', fun.pending, fun.pending?'⚠ needs fulfillment':'all clear', fun.pending?'var(--amber)':'var(--green2)'],
    ['Total Users', fun.users, `+${sgnToday} today · ${fun.conversion}% buy`, 'var(--purple)'],
    ['Sites Online', (t.online||0)+'/10', t.offline? t.offline+' DOWN':'all green', t.offline?'var(--red)':'var(--green2)'],
    ['Products Live', t.products||0, 'across fleet', 'var(--pink)'],
    ['Deposits', money(fun.deposits), 'store balances', 'var(--orange)'],
    ['Unique Buyers', fun.unique_customers, 'by email', 'var(--cyan)'],
  ];
  $('kpis').innerHTML = k.map(x=>`<div class="kpi" style="--kc:${x[3]}"><div class="l">${x[0]}</div><div class="v">${x[1]}</div><div class="d">${x[2]}</div></div>`).join('');

  /* charts */
  const hist = (state.history||[]).map(h=>({v:h.revenue}));
  area($('ch-rev'), hist, '#39c5cf');
  $('rev-total').textContent = hist.length? 'now '+money(hist[hist.length-1].v) : '';
  const days = [...new Set([...Object.keys(act.daily||{})])].sort();
  bars($('ch-act'), days, days.map(d=>(act.daily[d]||{}).orders||0), days.map(d=>(act.daily[d]||{}).signups||0));
  donut($('ch-users'), Object.entries(signups.per_site||{}).map(([sid,v])=>({k:(state.sites[sid]||{}).name||sid, v})).sort((a,b)=>b.v-a.v));
  const bp = state.btcpay||{};
  gauge($('ch-btc'), bp.sync_pct||0, 'blocks '+(bp.blocks||'?'));
  $('btc-sub').textContent = bp.synced ? 'fully synced · v'+(bp.version||'').split('+')[0] : 'syncing…';

  /* sites grid */
  $('fleet-tag').textContent = (t.online||0)+' online · '+(t.users||0)+' users';
  $('sites').innerHTML = sites.sort((a,b)=>a.site_id-b.site_id).map(s=>`
    <div class="site ${s.status!=='online'?'off':''}">
      <div class="hd"><span class="em">${s.emoji}</span>
        <div><div class="nm">${s.name}</div><div class="ip">${s.ip} · ${s.response_ms}ms</div></div>
        <span class="dot ${s.status==='online'?'on':'off'}"></span></div>
      <div class="nums">
        <div><div class="n">${s.users}</div><div class="t">users</div></div>
        <div><div class="n">${s.orders}</div><div class="t">orders</div></div>
        <div><div class="n">${money(s.revenue)}</div><div class="t">revenue</div></div>
      </div>
      <div class="ft"><span>${s.products} products</span>
        ${s.details&&s.details.pending_orders? `<span class="badge-pend">${s.details.pending_orders} pending</span>` : '<span class="badge-none">✓ clear</span>'}
      </div></div>`).join('');

  /* funnel */
  const fs = [['Users',fun.users],['Buyers',fun.buyers],['Orders',fun.orders],['Delivered',fun.delivered],['Revenue',money(fun.revenue)]];
  $('funnel').innerHTML = fs.map((f,i)=>`${i?'<span class="farrow">→</span>':''}<div class="fstep"><div class="n">${f[1]}</div><div class="l">${f[0]}</div></div>`).join('');

  /* feeds */
  $('ord-count').textContent = orders.length? orders.length+' total':'';
  $('orders').innerHTML = orders.length? orders.map(o=>`
    <div class="fi"><span class="ic">${o.emoji}</span>
      <span><b>${o.product.slice(0,34)}</b> <span class="amt">${money(o.amount)}</span><br>
      <span style="color:var(--dim)">${o.site} · <span class="st-${o.status==='delivered'?'d':'p'}">${o.status}</span> · ${o.email.split('@')[0]}@…</span></span>
      <span class="tt">${ago(o.date)}</span></div>`).join('') : '<div class="empty">no orders yet</div>';
  $('usr-count').textContent = Object.values(signups.tiers||{}).map((v,k)=>k+':'+v).join(' · ')||'';
  $('signups').innerHTML = signups.recent.length? signups.recent.map(u=>`
    <div class="fi"><span class="ic">${u.emoji}</span>
      <span><b>${u.username}</b> ${u.membership!=='free'?`<span class="mb">${u.membership}</span>`:''}<br>
      <span style="color:var(--dim)">${u.site} · ${u.orders} orders · ${money(u.balance)} bal</span></span>
      <span class="tt">${ago(u.joined)}</span></div>`).join('') : '<div class="empty">no signups yet</div>';
  $('events').innerHTML = (state.events||[]).slice(0,20).map(e=>`
    <div class="fi"><span>${e.detail}</span><span class="tt">${agoTs(e.timestamp)}</span></div>`).join('') || '<div class="empty">quiet…</div>';

  /* leaderboard */
  const maxRev = Math.max(...board.map(b=>b.revenue),1);
  $('board').innerHTML = `<tr><th></th><th>Site</th><th class="num">Revenue</th><th class="num">Orders</th><th class="num">24h</th><th class="num">AOV</th><th class="num">Users</th><th class="num">Pend</th></tr>` +
    board.map((b,i)=>`<tr><td class="rank">${i+1}</td><td>${b.emoji} ${b.site}<div class="bar" style="width:${(b.revenue/maxRev*100)|0}%"></div></td>
      <td class="num"><b>${money(b.revenue)}</b></td><td class="num">${b.orders}</td><td class="num">${b.orders_24h}</td>
      <td class="num">${money(b.aov)}</td><td class="num">${b.users}</td>
      <td class="num">${b.pending?`<span class="badge-pend">${b.pending}</span>`:'—'}</td></tr>`).join('');
  $('top-prod').innerHTML = `<tr><th>Product</th><th class="num">Sold</th><th class="num">Revenue</th><th class="num">Sites</th></tr>` +
    (top.length? top.map(p=>`<tr><td>${p.product.slice(0,30)}</td><td class="num">${p.orders}</td><td class="num">${money(p.revenue)}</td><td class="num">${p.sites}</td></tr>`).join('') : '<tr><td class="empty">no sales yet</td></tr>');

  /* btcpay stores */
  const stores = bp.stores||{};
  $('btcpay-tag').textContent = (bp.stores_with_wallet||0)+'/'+(bp.total_stores||10)+' wallets · '+(bp.total_invoices||0)+' invoices';
  $('stores').innerHTML = Object.entries(stores).map(([sid,st])=>{
    const nm = (state.sites[sid]||{}).emoji||''; const inv = st.invoices_by_status||{};
    return `<span class="store-chip">${nm} ${st.has_wallet?'<b>●</b>':'<span class="nw">○</span>'} ${st.invoice_count||0}inv <span style="color:var(--dim)">${inv.Settled?inv.Settled+'✓ ':''}${inv.New?inv.New+'⏳':''}</span></span>`;
  }).join('');
  const br = (state.btc_stats||{}).recent||[];
  $('btc-recent').innerHTML = br.length? br.map(x=>`<div class="fi"><span>₿ <span class="amt">${(x.btc||0).toLocaleString()}</span> sats-equiv<br><span style="color:var(--dim)" class="mono">${x.txid}</span></span><span class="tt">${agoTs(x.ts)}</span></div>`).join('') : '<div class="empty">no chain activity</div>';

  $('poll-age').textContent = state.last_poll||'—';
}
refresh();
setInterval(refresh, 20000);
</script>
</body>
</html>
