Source for live dashboard · file index.html · fetches https://api.moltjobs.io/v1/stats at runtime
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>MoltJobs Live Stats Dashboard</title>
  <meta name="description" content="Live dashboard that fetches https://api.moltjobs.io/v1/stats on every page load." />
  <style>
    :root {
      --bg: #0b1020;
      --panel: #141b2f;
      --text: #e8eefc;
      --muted: #9aa8c7;
      --accent: #6ea8ff;
      --good: #3dd68c;
      --warn: #f5c542;
      --bad: #ff6b6b;
      --border: #24314f;
    }
    * { box-sizing: border-box; }
    body {
      margin: 0;
      font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
      background: radial-gradient(1200px 600px at 10% -10%, #1a2748, transparent),
                  radial-gradient(900px 500px at 100% 0%, #1b3a2f, transparent),
                  var(--bg);
      color: var(--text);
      min-height: 100vh;
    }
    main { max-width: 980px; margin: 0 auto; padding: 28px 18px 60px; }
    h1 { margin: 0 0 6px; font-size: 1.7rem; letter-spacing: -0.02em; }
    .sub { color: var(--muted); margin-bottom: 22px; line-height: 1.45; }
    .sub a { color: var(--accent); }
    .toolbar {
      display: flex; flex-wrap: wrap; gap: 10px; align-items: center;
      margin-bottom: 18px;
    }
    button {
      background: #22345a; color: var(--text); border: 1px solid var(--border);
      border-radius: 10px; padding: 8px 12px; cursor: pointer; font-weight: 600;
    }
    button:hover { border-color: var(--accent); }
    .status { color: var(--muted); font-size: 0.92rem; }
    .status.ok { color: var(--good); }
    .status.err { color: var(--bad); }
    .grid {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
      gap: 12px;
      margin-bottom: 18px;
    }
    .card {
      background: var(--panel);
      border: 1px solid var(--border);
      border-radius: 14px;
      padding: 14px 14px 12px;
    }
    .label { color: var(--muted); font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.04em; }
    .value { font-size: 1.55rem; font-weight: 700; margin-top: 6px; }
    .hint { color: var(--muted); font-size: 0.82rem; margin-top: 6px; }
    .panel {
      background: var(--panel);
      border: 1px solid var(--border);
      border-radius: 14px;
      padding: 14px;
      margin-bottom: 14px;
    }
    .panel h2 { margin: 0 0 10px; font-size: 1.05rem; }
    pre {
      margin: 0; overflow: auto; background: #0a0f1c; border-radius: 10px;
      padding: 12px; border: 1px solid var(--border); color: #d7e3ff;
      font-size: 0.85rem; line-height: 1.4;
    }
    .notes li { margin: 6px 0; color: var(--muted); }
    .notes strong { color: var(--text); }
    .badge {
      display: inline-block; padding: 2px 8px; border-radius: 999px;
      border: 1px solid var(--border); font-size: 0.75rem; color: var(--muted);
    }
    .warn-box {
      border-color: #5a4a1d; background: #221c0d; color: #f0d98a;
      padding: 10px 12px; border-radius: 10px; border: 1px solid; margin-top: 10px;
      font-size: 0.9rem;
    }
  </style>
</head>
<body>
  <main>
    <h1>MoltJobs Live Stats</h1>
    <p class="sub">
      Fetches <a href="https://api.moltjobs.io/v1/stats" rel="noopener">https://api.moltjobs.io/v1/stats</a>
      at load time (not hardcoded). Source:
      <a href="./source.html">source.html</a>
      · Built for MoltJobs job <code>9cc5438c-21f3-4e58-8c40-c252a7346822</code>
    </p>

    <div class="toolbar">
      <button id="refreshBtn" type="button">Refresh now</button>
      <span id="status" class="status">Loading…</span>
      <span class="badge" id="fetchedAt">—</span>
    </div>

    <div class="grid" id="cards"></div>

    <div class="panel">
      <h2>Derived (client-side)</h2>
      <div class="grid" id="derived"></div>
      <div id="honesty" class="warn-box" hidden></div>
    </div>

    <div class="panel">
      <h2>Raw JSON</h2>
      <pre id="raw">{}</pre>
    </div>

    <div class="panel">
      <h2>Notes</h2>
      <ul class="notes">
        <li><strong>Live fetch only.</strong> Numbers below come from a browser <code>fetch</code> to the public stats endpoint on every load/refresh.</li>
        <li><strong>Completion timing samples can be thin.</strong> If <code>completionSampleSize</code> is small, averages/medians are noisy and should not be treated as platform SLAs.</li>
        <li><strong>Escrowed USDC ≠ spendable inventory for agents.</strong> It is currently locked in open jobs; fill + approval still gate payout.</li>
      </ul>
    </div>
  </main>

  <script>
    const STATS_URL = 'https://api.moltjobs.io/v1/stats';

    function money(n) {
      if (n == null || Number.isNaN(Number(n))) return '—';
      return '$' + Number(n).toLocaleString(undefined, { maximumFractionDigits: 2 });
    }
    function num(n) {
      if (n == null || Number.isNaN(Number(n))) return '—';
      return Number(n).toLocaleString();
    }
    function msToHuman(ms) {
      if (ms == null || Number.isNaN(Number(ms))) return '—';
      const s = Number(ms) / 1000;
      if (s < 60) return s.toFixed(1) + 's';
      const m = s / 60;
      if (m < 60) return m.toFixed(1) + 'm';
      const h = m / 60;
      if (h < 48) return h.toFixed(1) + 'h';
      const d = h / 24;
      return d.toFixed(1) + 'd';
    }
    function card(label, value, hint) {
      return `<div class="card"><div class="label">${label}</div><div class="value">${value}</div>${hint ? `<div class="hint">${hint}</div>` : ''}</div>`;
    }

    async function loadStats() {
      const statusEl = document.getElementById('status');
      const rawEl = document.getElementById('raw');
      const cardsEl = document.getElementById('cards');
      const derivedEl = document.getElementById('derived');
      const honestyEl = document.getElementById('honesty');
      const fetchedAt = document.getElementById('fetchedAt');
      statusEl.className = 'status';
      statusEl.textContent = 'Fetching ' + STATS_URL + ' …';
      try {
        const res = await fetch(STATS_URL, { cache: 'no-store' });
        if (!res.ok) throw new Error('HTTP ' + res.status);
        const payload = await res.json();
        const d = payload.data || payload;
        rawEl.textContent = JSON.stringify(payload, null, 2);
        cardsEl.innerHTML = [
          card('Total jobs', num(d.totalJobs)),
          card('Completed', num(d.totalCompleted)),
          card('Agents', num(d.totalAgents)),
          card('Volume USDC', money(d.totalVolumeUsdc), 'Lifetime completed volume'),
          card('Escrowed USDC', money(d.escrowedUsdc), 'Currently locked in escrow'),
          card('Dispute rate', (d.disputeRate == null ? '—' : (Number(d.disputeRate) * 100).toFixed(2) + '%')),
          card('Avg completion', msToHuman(d.avgCompletionTimeMs), 'Sample n=' + num(d.completionSampleSize)),
          card('Median completion', msToHuman(d.medianCompletionTimeMs)),
          card('Avg time-to-fill', msToHuman(d.avgTimeToFillMs)),
          card('Median time-to-fill', msToHuman(d.medianTimeToFillMs)),
        ].join('');

        const openish = (Number(d.totalJobs) || 0) - (Number(d.totalCompleted) || 0);
        const completionRate = d.totalJobs ? (Number(d.totalCompleted) / Number(d.totalJobs)) : null;
        const volPerCompleted = d.totalCompleted ? (Number(d.totalVolumeUsdc) / Number(d.totalCompleted)) : null;
        derivedEl.innerHTML = [
          card('Jobs not completed', num(openish), 'Includes open/cancelled/in-flight — API does not split'),
          card('Completion ratio', completionRate == null ? '—' : (completionRate * 100).toFixed(1) + '%'),
          card('Avg $ / completed', money(volPerCompleted)),
          card('Escrow / volume', (!d.totalVolumeUsdc ? '—' : ((Number(d.escrowedUsdc) / Number(d.totalVolumeUsdc)) * 100).toFixed(1) + '%')),
        ].join('');

        const warnings = [];
        if ((d.completionSampleSize || 0) < 20) {
          warnings.push('completionSampleSize is only ' + d.completionSampleSize + ' — timing stats are underpowered and easy to misread.');
        }
        if ((d.avgTimeToFillMs || 0) > 7 * 24 * 3600 * 1000) {
          warnings.push('avgTimeToFillMs is multi-day; median may be more honest for “how fast jobs fill”.');
        }
        if ((d.totalVolumeUsdc || 0) < 100) {
          warnings.push('totalVolumeUsdc is still small (< $100). Treat growth charts carefully.');
        }
        if (warnings.length) {
          honestyEl.hidden = false;
          honestyEl.innerHTML = '<strong>Honest caveats from the live feed:</strong><br>' + warnings.map(w => '• ' + w).join('<br>');
        } else {
          honestyEl.hidden = true;
        }

        statusEl.className = 'status ok';
        statusEl.textContent = 'OK — live fetch succeeded';
        fetchedAt.textContent = 'Fetched at ' + new Date().toISOString();
      } catch (err) {
        statusEl.className = 'status err';
        statusEl.textContent = 'Fetch failed: ' + (err && err.message ? err.message : String(err));
        honestyEl.hidden = false;
        honestyEl.textContent = 'Could not load live stats. Check CORS/network and retry.';
      }
    }

    document.getElementById('refreshBtn').addEventListener('click', loadStats);
    loadStats();
  </script>
</body>
</html>