// Live-data overlay. Fetches what's actually available from solverd /
// orderbookd / btc-core / zebrad and produces a partial overlay that
// app.jsx merges onto an N/A baseline. Unavailable sources show N/A instead
// of falling back to synthetic placeholder data.
//
// Endpoints (all proxied through the local serve.mjs so no CORS / auth in browser):
//   GET  /api/quote?direction=btc-to-zec&amountSats=1000000     → SolverQuote
//   GET  /api/orders                                            → Order[]
//   GET  /health/solverd | /health/orderbookd | /health/indexerd
//   POST /btc-rpc   { method, params }                          → JSON-RPC
//   POST /zec-rpc   { method, params }                          → JSON-RPC

const { useEffect, useRef, useState } = React;

// The monitor is normally served from its own host. The GCP stack also
// exposes it under /solver/ on the app host while DNS for the solver host is
// being set up. Keep API calls relative to that mount when needed.
const API_BASE = (() => {
  const p = window.location.pathname || '/';
  return p === '/solver' || p.startsWith('/solver/') ? '/solver' : '';
})();
const apiPath = (path) => `${API_BASE}${path}`;

const NA = "N/A";
const isNum = (v) => v !== null && v !== undefined && v !== "" && Number.isFinite(Number(v));
const numOrNull = (v) => isNum(v) ? Number(v) : null;
const hexByte = (n) => Number(n).toString(16).padStart(2, "0");
function hexish(v) {
  if (!v || v === NA) return null;
  if (typeof v === "string") return v;
  if (Array.isArray(v)) return v.map(hexByte).join("");
  return null;
}

function unavailableHealth() {
  return {
    btc: {
      rpc: { status: "err", latencyMs: null, endpoint: NA },
      height: null,
      mempoolFee: { sats_vb_econ: null, sats_vb_fast: null, sats_vb_min: null },
      walletBalance: null,
      utxoCount: null,
      reservedSats: null,
      pendingSpends: null,
      lastBlockMs: null,
    },
    zec: {
      rpc: { status: "err", latencyMs: null, endpoint: NA },
      lwd: { status: "err", latencyMs: null, endpoint: NA },
      tipHeight: null,
      scanHeight: null,
      scanLag: null,
      walletBalance: null,
      noteCount: null,
      pendingSpends: null,
      reorgDepthSeen24h: null,
    },
    quotes: {
      source: NA,
      btcUsd: null,
      zecUsd: null,
      spreadBps: null,
      ttlSecs: null,
      observedAgo: null,
      stale: true,
      midZatsPerSat: null,
    },
    preflight: {
      lastRun: null,
      passed: null,
      total: null,
      mainnetEnabled: null,
      keystoreMode: NA,
      strictMode: null,
      ok: null,
      error: null,
    },
    process: {
      uptime: null,
      memoryMb: null,
      memoryLimitMb: null,
      cpu: null,
      version: NA,
      host: NA,
      solverId: NA,
      btcNetwork: NA,
      zecNetwork: NA,
    },
    db: {
      orderbookSize: null,
      btcMetaSize: null,
      zecWalletSize: null,
      lastBackup: null,
    },
    services: {},
  };
}

function emptyLiveOverlay() {
  return {
    health: unavailableHealth(),
    orders: [],
    swaps: [],
    alerts: [],
    logs: [],
    spark: {
      throughput: [],
      btcBalance: [],
      zecBalance: [],
      pnl: [],
    },
    aggregate: {
      active: null,
      completed24h: null,
      refunded24h: null,
      pnl24h: null,
      avgFillSec: null,
      btcUsd: null,
      zecUsd: null,
      impliedZecPerBtc: null,
    },
  };
}

async function fetchTimed(input, init = {}, timeoutMs = 2500) {
  const ctrl = new AbortController();
  const timer = setTimeout(() => ctrl.abort(), timeoutMs);
  const t0 = performance.now();
  try {
    const r = await fetch(input, { ...init, signal: ctrl.signal });
    const elapsed = performance.now() - t0;
    return { ok: r.ok, status: r.status, body: await r.text(), elapsedMs: Math.round(elapsed) };
  } catch (err) {
    return { ok: false, status: 0, body: String(err), elapsedMs: Math.round(performance.now() - t0) };
  } finally {
    clearTimeout(timer);
  }
}

async function jsonRpc(path, method, params = []) {
  const r = await fetchTimed(path, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ jsonrpc: '1.0', id: 'monitor', method, params }),
  });
  if (!r.ok) return { ok: false, latencyMs: r.elapsedMs, error: r.body };
  try {
    const j = JSON.parse(r.body);
    if (j && j.ok === false) return { ok: false, latencyMs: r.elapsedMs, error: j.error || 'rpc unreachable' };
    if (j.error) return { ok: false, latencyMs: r.elapsedMs, error: j.error.message || JSON.stringify(j.error) };
    return { ok: true, latencyMs: r.elapsedMs, result: j.result };
  } catch (err) {
    return { ok: false, latencyMs: r.elapsedMs, error: String(err) };
  }
}

async function fetchHealth(svcPath) {
  const r = await fetchTimed(svcPath);
  return {
    status: r.ok ? 'ok' : 'err',
    latencyMs: r.elapsedMs,
    body: r.body,
  };
}

async function fetchQuote() {
  // Use a small mainnet-safe sample. The live lane currently caps quotes at
  // 1,000,000 sats, so a 1 BTC probe would 400 and leave the quote panel N/A.
  const r = await fetchTimed(apiPath('/api/quote?direction=btc-to-zec&amountSats=1000000'));
  if (!r.ok) return { ok: false, error: r.body };
  try {
    const q = JSON.parse(r.body);
    if (q && q.ok === false) return { ok: false, error: q.error || 'quote unreachable' };
    if (q && q.error) return { ok: false, error: q.error };
    return { ok: true, quote: q };
  } catch (err) {
    return { ok: false, error: String(err) };
  }
}

async function fetchOrders() {
  const r = await fetchTimed(apiPath('/api/orders'));
  if (!r.ok) return { ok: false, error: r.body };
  try {
    const arr = JSON.parse(r.body);
    if (arr && arr.ok === false) return { ok: false, error: arr.error || 'orders unreachable' };
    return { ok: true, orders: Array.isArray(arr) ? arr : [] };
  } catch (err) {
    return { ok: false, error: String(err) };
  }
}

function mapOrder(o) {
  const dir = (o.direction || '').replace(/_/g, '-').toLowerCase();
  return {
    id: o.id || NA,
    direction: dir === 'btctozec' ? 'btc-to-zec' : dir === 'zectobtc' ? 'zec-to-btc' : dir,
    amountSats: numOrNull(o.amountSats ?? o.amount_sats),
    status: o.status || NA,
    createdAt: numOrNull(o.createdAt ?? o.created_at),
    counterparty: o.solverId || o.solver_id || NA,
    quoteId: o.acceptedQuoteId || o.accepted_quote_id || NA,
  };
}

async function fetchJson(path) {
  const r = await fetchTimed(path);
  if (!r.ok) return { ok: false, error: r.body };
  try {
    const j = JSON.parse(r.body);
    if (j && j.ok === false) return { ok: false, error: j.error || `${path} unreachable` };
    if (j && j.error && typeof j.error === 'string') return { ok: false, error: j.error };
    return { ok: true, data: j };
  } catch (err) {
    return { ok: false, error: String(err) };
  }
}

async function buildOverlay() {
  const [
    quoteR, ordersR,
    solverdH, orderbookdH, indexerdH,
    btcChain, btcFee,
    zecChain, zecInfo,
    swapsR, walletsR, preflightR, processR,
  ] = await Promise.all([
    fetchQuote(),
    fetchOrders(),
    fetchHealth(apiPath('/health/solverd')),
    fetchHealth(apiPath('/health/orderbookd')),
    fetchHealth(apiPath('/health/indexerd')),
    jsonRpc(apiPath('/btc-rpc'), 'getblockchaininfo'),
    jsonRpc(apiPath('/btc-rpc'), 'estimatesmartfee', [6]),
    jsonRpc(apiPath('/zec-rpc'), 'getblockchaininfo'),
    jsonRpc(apiPath('/zec-rpc'), 'getinfo'),
    fetchJson(apiPath('/api/swaps?status=all&limit=200')),
    fetchJson(apiPath('/api/wallets')),
    fetchJson(apiPath('/api/preflight')),
    fetchJson(apiPath('/api/process')),
  ]);

  const sources = {};
  const overlay = emptyLiveOverlay();

  // Quote
  if (quoteR.ok && quoteR.quote) {
    const q = quoteR.quote;
    const observedAgo = Math.max(0, Math.floor((Date.now() - Number(q.observedAtMs || 0)) / 1000));
    const ttlSecs = Math.max(0, Math.floor((Number(q.expiresAtMs || 0) - Number(q.observedAtMs || 0)) / 1000));
    overlay.health.quotes = {
      ...overlay.health.quotes,
      source: q.source || NA,
      spreadBps: numOrNull(q.spreadBps),
      ttlSecs,
      observedAgo,
      stale: observedAgo > ttlSecs,
      // mid_zats_per_sat: 1e8 zats/ZEC * (BTC/ZEC ratio in sat units)
      midZatsPerSat: numOrNull(q.midZatsPerSat),
    };
    overlay.aggregate.impliedZecPerBtc = numOrNull(q.midZatsPerSat);
    sources.quote = { ok: true, latencyMs: 0 };
  } else {
    sources.quote = { ok: false, error: quoteR.error };
  }

  // Orders
  if (ordersR.ok) {
    overlay.orders = ordersR.orders.map(mapOrder);
    sources.orders = { ok: true, count: ordersR.orders.length };
  } else {
    sources.orders = { ok: false, error: ordersR.error };
  }

  // Service health
  overlay.health.services = {
    solverd:    { status: solverdH.status,    latencyMs: solverdH.latencyMs },
    orderbookd: { status: orderbookdH.status, latencyMs: orderbookdH.latencyMs },
    indexerd:   { status: indexerdH.status,   latencyMs: indexerdH.latencyMs },
  };
  sources.solverd    = { ok: solverdH.status    === 'ok', latencyMs: solverdH.latencyMs };
  sources.orderbookd = { ok: orderbookdH.status === 'ok', latencyMs: orderbookdH.latencyMs };
  sources.indexerd   = { ok: indexerdH.status   === 'ok', latencyMs: indexerdH.latencyMs };

  // BTC chain
  if (btcChain.ok && btcChain.result) {
    overlay.health.btc.height = numOrNull(btcChain.result.blocks);
    overlay.health.btc.lastBlockMs = isNum(btcChain.result.time)
      ? Math.max(0, Date.now() - Number(btcChain.result.time) * 1000)
      : null;
    overlay.health.btc.rpc = {
      status: 'ok',
      latencyMs: btcChain.latencyMs,
      endpoint: 'btc-core (proxied via /btc-rpc)',
    };
    sources.btcRpc = { ok: true, latencyMs: btcChain.latencyMs, height: overlay.health.btc.height };
  } else {
    overlay.health.btc.rpc = { status: 'err', latencyMs: btcChain.latencyMs, endpoint: btcChain.error || 'unreachable' };
    sources.btcRpc = { ok: false, error: btcChain.error };
  }
  if (btcFee.ok && btcFee.result?.feerate != null) {
    // bitcoin-core returns BTC/kB; convert to sat/vB.
    const satsPerVb = Math.max(1, Math.round((Number(btcFee.result.feerate) * 1e8) / 1000));
    overlay.health.btc.mempoolFee = {
      sats_vb_econ: satsPerVb,
      sats_vb_fast: satsPerVb * 2,
      sats_vb_min: 1,
    };
    sources.btcFee = { ok: true, satsPerVb };
  } else {
    sources.btcFee = { ok: false };
  }

  // ZEC chain
  if (zecChain.ok && zecChain.result) {
    overlay.health.zec.tipHeight = numOrNull(zecChain.result.blocks);
    overlay.health.zec.rpc = {
      status: 'ok',
      latencyMs: zecChain.latencyMs,
      endpoint: 'zebrad (proxied via /zec-rpc)',
    };
    sources.zecRpc = { ok: true, latencyMs: zecChain.latencyMs, height: overlay.health.zec.tipHeight };
  } else {
    overlay.health.zec.rpc = { status: 'err', latencyMs: zecChain.latencyMs, endpoint: zecChain.error || 'unreachable' };
    sources.zecRpc = { ok: false, error: zecChain.error };
  }
  if (zecInfo.ok && zecInfo.result) {
    overlay.health.zec.lwd = {
      status: 'ok',
      latencyMs: zecInfo.latencyMs,
      endpoint: zecInfo.result.subversion || 'zebrad',
    };
  }

  // Swaps (replaces mocked swap rows; FSMHeatmap, Swaps view, detail all read from this)
  if (swapsR.ok && Array.isArray(swapsR.data?.swaps)) {
    const orderById = new Map(ordersR.ok ? ordersR.orders.map((o) => [o.id, o]) : []);
    const rowsForDetail = swapsR.data.swaps.slice(0, 50);
    const detailRs = await Promise.all(rowsForDetail.map((s) =>
      fetchJson(apiPath(`/api/swaps/${encodeURIComponent(s.swapId)}`))
    ));
    const quoteRs = await Promise.all(rowsForDetail.map((s) =>
      fetchJson(apiPath(`/api/orders/${encodeURIComponent(s.swapId)}/quotes`))
    ));
    const detailById = new Map();
    const quoteById = new Map();
    rowsForDetail.forEach((s, i) => {
      if (detailRs[i]?.ok) detailById.set(s.swapId, detailRs[i].data);
      if (quoteRs[i]?.ok && Array.isArray(quoteRs[i].data)) {
        const order = orderById.get(s.swapId);
        const acceptedId = order?.acceptedQuoteId || order?.accepted_quote_id;
        quoteById.set(
          s.swapId,
          quoteRs[i].data.find((q) => q.quoteId === acceptedId) || quoteRs[i].data[0] || null
        );
      }
    });
    overlay.swaps = swapsR.data.swaps.map((s) => mapSwap(s, {
      detail: detailById.get(s.swapId),
      order: orderById.get(s.swapId),
      quote: quoteById.get(s.swapId),
      btcTip: overlay.health.btc.height,
      zecTip: overlay.health.zec.tipHeight,
    }));
    sources.swaps = { ok: true, count: swapsR.data.swaps.length };
    sources.swapDetails = { ok: true, count: detailById.size };
  } else {
    sources.swaps = { ok: false, error: swapsR.error };
  }

  // Wallets — overlay btc/zec wallet balances + scan info
  if (walletsR.ok && walletsR.data) {
    const btc = walletsR.data.btc || {};
    if (btc.balanceSats != null) overlay.health.btc.walletBalance = numOrNull(btc.balanceSats);
    if (btc.utxoCount   != null) overlay.health.btc.utxoCount     = numOrNull(btc.utxoCount);
    if (btc.pendingSpends != null) overlay.health.btc.pendingSpends = numOrNull(btc.pendingSpends);
    const zec = walletsR.data.zec || {};
    if (zec.balance) {
      if (zec.balance.spendableZats != null) overlay.health.zec.walletBalance = numOrNull(zec.balance.spendableZats);
      if (zec.balance.noteCount     != null) overlay.health.zec.noteCount     = numOrNull(zec.balance.noteCount);
      if (zec.balance.pendingSpendZats != null) overlay.health.zec.pendingSpends = zec.pendingSpends?.length ?? 0;
    }
    if (zec.scan) {
      const scannedHeight = numOrNull(zec.scan.scannedHeight);
      // A scannedHeight of 0 is how the current wallet endpoint reports an
      // uninitialized/unavailable Orchard scan. Treat it as N/A rather than
      // "millions of blocks behind".
      if (scannedHeight != null && scannedHeight > 0) {
        overlay.health.zec.scanHeight = scannedHeight;
      }
      if (zec.scan.confirmedHeight != null && Number(zec.scan.confirmedHeight) > 0 && overlay.health.zec.tipHeight == null) {
        overlay.health.zec.tipHeight = numOrNull(zec.scan.confirmedHeight);
      }
      if (overlay.health.zec.scanHeight != null && overlay.health.zec.tipHeight != null) {
        overlay.health.zec.scanLag = Math.max(0, overlay.health.zec.tipHeight - overlay.health.zec.scanHeight);
      }
    }
    sources.wallets = { ok: true };
  } else {
    sources.wallets = { ok: false, error: walletsR.error };
  }

  // Preflight
  if (preflightR.ok && preflightR.data) {
    const p = preflightR.data;
    overlay.health.preflight = {
      passed: p.ok ? 1 : 0,
      total: 1,
      mainnetEnabled: p.network?.mainnetSwapsEnabled ?? false,
      strictMode: p.strictPreflight ?? false,
      keystoreMode: p.walletBackends?.zecKeystorePassphraseSource ?? NA,
      lastRun: Date.now(),
      ok: !!p.ok,
      error: p.error || null,
      raw: p,
    };
    sources.preflight = { ok: true, passing: !!p.ok };
  } else {
    sources.preflight = { ok: false, error: preflightR.error };
  }

  // Process info
  if (processR.ok && processR.data) {
    const p = processR.data;
    overlay.health.process = {
      version: `${p.binary || 'zwap-solverd'} ${p.version || ''}`.trim(),
      host: p.host || NA,
      uptime: numOrNull(p.uptimeSec),
      memoryMb: numOrNull(p.memMb),
      memoryLimitMb: null,
      cpu: null,
      solverId: p.solverId || NA,
      btcNetwork: p.btcNetwork || NA,
      zecNetwork: p.zecNetwork || NA,
    };
    sources.process = { ok: true, uptimeSec: p.uptimeSec };
  } else {
    sources.process = { ok: false, error: processR.error };
  }

  return { overlay, sources, ts: Date.now() };
}

// Map a /api/swaps row (camelCase, server-side schema) into the dashboard's
// existing swap shape so views/heatmap/detail keep working unchanged.
function mapSwap(s, ctx = {}) {
  const detail = ctx.detail || {};
  const order = ctx.order || {};
  const quote = ctx.quote || {};
  const mat = detail.material || s.material || {};
  const aliceSession = mat.alice_session || {};
  const phase0Initiator = mat.phase0_initiator_payload || {};
  const phase0Responder = mat.phase0_responder_payload || {};
  const dir = (s.direction || '').replace(/_/g, '-');
  const txids = s.txids || {};
  const lockTxid       = txids.lockTxid       || txids.btc_lock       || txids.lock_tx        || null;
  const zecDepositTxid = txids.zecDepositTxid || txids.zec_deposit    || null;
  const buyTxid        = txids.buyTxid        || txids.btc_buy        || txids.lock_spend_tx  || null;
  const orchardTxid    = txids.orchardTxid    || txids.zec_claim      || null;
  const refundTxid     = txids.refundTxid     || txids.btc_refund     || null;
  const zecAmountZats = s.zecAmountZats ?? order.quoteZecAmountZats ?? quote.zecAmountZats;
  const rate = numOrNull(quote.quotedZatsPerSat)
    ?? (isNum(zecAmountZats) && isNum(s.btcAmountSats) && Number(s.btcAmountSats) > 0
      ? Number(zecAmountZats) / Number(s.btcAmountSats)
      : null);
  return {
    id: s.swapId || NA,
    direction: dir || NA,
    role: s.role || NA,
    counterparty: s.counterparty || order.counterparty || NA,
    solverId: s.solverId || order.solverId || order.solver_id || quote.solverId || NA,
    state: s.state || 'created',
    branch: s.state?.includes('refund') ? 'refund'
          : s.state?.includes('claim')  ? 'claim'
          : 'happy',
    stuck: false,
    btcSats: numOrNull(s.btcAmountSats),
    zecZats: numOrNull(zecAmountZats),
    rateZecPerBtc: rate,
    spreadBps: numOrNull(quote.spreadBps),
    pnlUsd: null,
    usdValue: null,
    startedAt: numOrNull(s.createdAtMs),
    updatedAt: numOrNull(s.updatedAtMs),
    lockHeight: null,
    timelock0_blocks: numOrNull(aliceSession.timelock0),
    timelock1_blocks: numOrNull(aliceSession.timelock1),
    csvDeadline: numOrNull(s.expiresAtMs),
    csv2Deadline: null,
    lockTxid, zecDepositTxid, buyTxid, orchardTxid, refundTxid,
    events: [],
    quoteId: order.acceptedQuoteId || order.accepted_quote_id || quote.quoteId || NA,
    aliceAddress: NA,
    bobAddress: NA,
    swapHash: hexish(aliceSession.swap_hash) || s.swapId || NA,
    btcTip: ctx.btcTip ?? null,
    zecTip: ctx.zecTip ?? null,
    materialStatus: {
      kA: aliceSession.k_a_be ? "redacted" : NA,
      kB: mat.bob_session?.k_b_be ? "redacted" : NA,
      dleqProof: (phase0Initiator.dleqProof || phase0Responder.dleqProof) ? "present" : NA,
      hashbind: (phase0Responder.hashBindingProof || phase0Responder.hashCommitment) ? "present" : NA,
      lockPubkey: phase0Initiator.lockPubkey || hexish(aliceSession.lock_pubkey) || NA,
      refundPubkey: phase0Initiator.refundPubkey || hexish(aliceSession.refund_pubkey) || NA,
    },
    failureReason: s.failureReason || null,
  };
}

function deepMerge(base, layer) {
  if (!layer) return base;
  if (Array.isArray(layer)) return layer;
  if (typeof layer !== 'object') return layer;
  const out = { ...base };
  for (const k of Object.keys(layer)) {
    const v = layer[k];
    if (v === undefined) continue;
    if (v === null) {
      out[k] = null;
      continue;
    }
    if (typeof v === 'object' && !Array.isArray(v) && typeof base[k] === 'object' && base[k] != null) {
      out[k] = deepMerge(base[k], v);
    } else {
      out[k] = v;
    }
  }
  return out;
}

function mergeOverlay(mock, live) {
  if (!live || !live.overlay) live = { overlay: emptyLiveOverlay(), sources: {}, ts: 0 };
  const out = { ...mock };
  out.health = deepMerge(mock.health, live.overlay.health);
  out.alerts = Array.isArray(live.overlay.alerts) ? live.overlay.alerts : [];
  out.logs = Array.isArray(live.overlay.logs) ? live.overlay.logs : [];
  out.spark = live.overlay.spark || { throughput: [], btcBalance: [], zecBalance: [], pnl: [] };
  out.aggregate = {
    ...mock.aggregate,
    ...live.overlay.aggregate,
  };
  if (Array.isArray(live.overlay.orders)) {
    out.orders = live.overlay.orders;
  }
  if (Array.isArray(live.overlay.swaps)) {
    out.swaps = live.overlay.swaps;
    // Recompute live counts even when there are zero swaps; do not fall back
    // to demo rows.
    const TERMINAL = window.ZW.TERMINAL;
    out.aggregate = {
      ...out.aggregate,
      active: live.overlay.swaps.filter((s) => !TERMINAL.has(s.state)).length,
      completed24h: live.overlay.swaps.filter((s) => s.state === 'alice_redeemed').length,
      refunded24h: live.overlay.swaps.filter((s) =>
        window.ZW.REFUND_BRANCH.includes(s.state) || window.ZW.CLAIM_BRANCH.includes(s.state)
      ).length,
    };
  }
  return out;
}

function useLiveOverlay({ tickRateSec = 5 } = {}) {
  const [state, setState] = useState({ overlay: emptyLiveOverlay(), sources: {}, ts: 0 });
  const inFlight = useRef(false);

  useEffect(() => {
    let cancelled = false;
    async function tick() {
      if (inFlight.current) return;
      inFlight.current = true;
      try {
        const next = await buildOverlay();
        if (!cancelled) setState(next);
      } finally {
        inFlight.current = false;
      }
    }
    tick();
    const id = setInterval(tick, Math.max(1000, tickRateSec * 1000));
    return () => { cancelled = true; clearInterval(id); };
  }, [tickRateSec]);

  return state;
}

// Status pill — shows a list of which sources are live vs failing, ticks
// when the last refresh happened.
function LiveStatusPill({ sources, ts }) {
  const entries = Object.entries(sources || {});
  const live = entries.filter(([, v]) => v && v.ok);
  const failing = entries.filter(([, v]) => v && !v.ok);
  const tone = live.length === 0 ? 'neutral' : failing.length === 0 ? 'ok' : 'warn';
  const label = live.length === 0
    ? 'waiting for live data'
    : `${live.length} live · ${failing.length} unavailable`;
  const title = entries
    .map(([k, v]) => `${k}: ${v.ok ? 'live' : 'unavailable'} ${v.latencyMs != null ? `(${v.latencyMs}ms)` : ''} ${v.error ? '— ' + v.error : ''}`)
    .join('\n') + (ts ? `\nlast refresh ${new Date(ts).toLocaleTimeString()}` : '');
  return (
    <span title={title}>
      <Badge tone={tone} subtle>
        <StatusDot status={tone === 'neutral' ? 'idle' : tone} size={6} blink={tone !== 'neutral'} />
        {label}
      </Badge>
    </span>
  );
}

Object.assign(window, { useLiveOverlay, mergeOverlay, LiveStatusPill, buildLiveOverlay: buildOverlay });
