← back to Ventura Claw

server/public/admin-services-leads.html

277 lines

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Services Leads — VenturaClaw Admin</title>
<script>
  // Anti-flash: apply theme before paint so light-mode users don't see a black flash
  (function () {
    try {
      var t = localStorage.getItem('vc-theme') || 'dark';
      if (t === 'light') document.documentElement.setAttribute('data-theme', 'light');
    } catch (e) {}
  })();
</script>
<style>
  :root {
    --bg: #0e0e10; --bg-2: #15151a; --bg-3: #1c1c22;
    --rule: #2a2a32; --rule-2: #3a3a45;
    --ink: #f4f1ea; --ink-soft: #d8d2c5; --ink-mute: #8b857a; --ink-faint: #5d574c;
    --gold: #d4a04a; --gold-glow: #e6b96a;
    --good: #6ec081; --bad: #c66c4d;
    --serif: "Cormorant Garamond", Georgia, serif;
    --mono: "JetBrains Mono", "SFMono-Regular", Menlo, monospace;
    --sans: -apple-system, "SF Pro Text", system-ui, sans-serif;
  }
  html[data-theme="light"] {
    --bg: #faf8f3; --bg-2: #f1ece1; --bg-3: #e8e0cf;
    --rule: #d6cfbd; --rule-2: #c3b9a2;
    --ink: #1a1a1a; --ink-soft: #383530; --ink-mute: #6a655a; --ink-faint: #908a7d;
    --gold: #8a6520; --gold-glow: #ad8333;
    --good: #3d7a4a; --bad: #a04a32;
  }
  html, body { transition: background-color 200ms ease, color 200ms ease; }
  /* sun/moon toggle */
  .theme-toggle {
    background: var(--bg-3); color: var(--ink-mute); border: 1px solid var(--rule);
    width: 32px; height: 32px; border-radius: 50%; cursor: pointer;
    display: inline-flex; align-items: center; justify-content: center;
    font-size: 14px; line-height: 1; padding: 0; margin-left: 14px;
    transition: all 200ms;
  }
  .theme-toggle:hover { color: var(--gold); border-color: var(--gold); transform: rotate(20deg); }
  .theme-toggle .moon { display: inline; }
  .theme-toggle .sun  { display: none; }
  html[data-theme="light"] .theme-toggle .moon { display: none; }
  html[data-theme="light"] .theme-toggle .sun  { display: inline; }
  *, *::before, *::after { box-sizing: border-box; }
  html, body { margin: 0; background: var(--bg); color: var(--ink); font-family: var(--sans); font-weight: 300; min-height: 100vh; }
  a { color: var(--gold); text-decoration: none; }
  a:hover { color: var(--gold-glow); }

  header {
    padding: 22px 36px; display: flex; justify-content: space-between; align-items: center;
    border-bottom: 1px solid var(--rule);
    background: rgba(14,14,16,0.85); backdrop-filter: blur(8px);
    position: sticky; top: 0; z-index: 50;
  }
  .brand { display: flex; gap: 12px; align-items: baseline; }
  .brand .name { font-family: var(--serif); font-size: 22px; font-weight: 500; }
  .brand .name em { color: var(--gold); font-style: italic; }
  .brand .crumb { font-family: var(--mono); font-size: 10px; letter-spacing: 0.18em; text-transform: uppercase; color: var(--ink-mute); }
  .brand .crumb::before { content: "/ "; color: var(--ink-faint); }
  nav { display: flex; gap: 22px; align-items: center; }
  nav a { font-family: var(--mono); font-size: 11px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--ink-mute); }
  nav a:hover { color: var(--gold); }

  main { max-width: 1200px; margin: 0 auto; padding: 36px; }
  h1 { font-family: var(--serif); font-weight: 400; font-size: 38px; letter-spacing: -0.012em; margin: 0 0 8px; }
  h1 em { color: var(--gold); font-style: italic; }
  .subhead { color: var(--ink-mute); font-family: var(--mono); font-size: 11px; letter-spacing: 0.14em; text-transform: uppercase; margin-bottom: 36px; }
  .subhead b { color: var(--gold); font-family: var(--serif); font-style: italic; font-weight: 400; font-size: 14px; letter-spacing: 0; text-transform: none; margin-right: 4px; }

  .toolbar {
    display: flex; gap: 12px; margin-bottom: 18px; align-items: center; flex-wrap: wrap;
  }
  .toolbar input {
    flex: 1; min-width: 240px;
    background: var(--bg-2); color: var(--ink); border: 1px solid var(--rule); border-radius: 3px;
    padding: 9px 14px; font-family: var(--sans); font-size: 13px; font-weight: 300; outline: none;
  }
  .toolbar input:focus { border-color: var(--gold); }
  .toolbar select {
    background: var(--bg-2); color: var(--ink); border: 1px solid var(--rule); border-radius: 3px;
    padding: 9px 14px; font-family: var(--mono); font-size: 11px; letter-spacing: 0.06em; outline: none;
  }
  .toolbar .refresh-btn {
    background: var(--bg-2); color: var(--ink-mute); border: 1px solid var(--rule); border-radius: 3px;
    padding: 9px 16px; font-family: var(--mono); font-size: 10px; letter-spacing: 0.16em; text-transform: uppercase; cursor: pointer;
    transition: all 200ms;
  }
  .toolbar .refresh-btn:hover { color: var(--gold); border-color: var(--gold); }
  .stats {
    margin-left: auto;
    font-family: var(--mono); font-size: 10px; letter-spacing: 0.12em; color: var(--ink-mute);
  }
  .stats b { color: var(--gold); font-family: var(--serif); font-style: italic; font-weight: 400; font-size: 14px; letter-spacing: 0; margin: 0 4px; }

  table {
    width: 100%; border-collapse: collapse;
    background: var(--bg-2); border: 1px solid var(--rule);
    font-family: var(--mono); font-size: 12px;
  }
  th, td {
    padding: 12px 14px; text-align: left; vertical-align: top;
    border-bottom: 1px solid var(--rule);
  }
  th {
    background: var(--bg-3); color: var(--gold);
    font-weight: 500; letter-spacing: 0.08em; text-transform: uppercase; font-size: 10px;
  }
  tr:hover td { background: var(--bg-3); }
  td.email { color: var(--ink); }
  td.email a { color: var(--gold); }
  td.service {
    font-family: var(--serif); font-size: 14px; color: var(--ink); font-weight: 500; letter-spacing: -0.005em;
  }
  td.notes {
    color: var(--ink-soft); font-family: var(--sans); font-size: 13px;
    max-width: 320px; white-space: pre-wrap; line-height: 1.45;
  }
  td.at { color: var(--ink-mute); font-size: 11px; white-space: nowrap; }
  td.id { color: var(--ink-faint); font-size: 10px; }
  .empty {
    padding: 80px 36px; text-align: center; color: var(--ink-mute);
    border: 1px dashed var(--rule); background: var(--bg-2);
  }
  .empty .glyph { font-size: 64px; color: var(--ink-faint); display: block; margin-bottom: 14px; }
  .empty .msg { font-family: var(--serif); font-size: 22px; color: var(--ink-soft); margin-bottom: 6px; }
  .empty .sub { font-family: var(--mono); font-size: 11px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--ink-faint); }

  .badge { display: inline-block; padding: 3px 8px; border-radius: 999px; font-size: 9px; letter-spacing: 0.12em; text-transform: uppercase; }
  .badge.new { background: rgba(110,192,129,0.1); border: 1px solid var(--good); color: var(--good); }
  .badge.old { background: var(--bg-3); border: 1px solid var(--rule); color: var(--ink-mute); }

  .err { color: var(--bad); padding: 14px; border: 1px solid var(--bad); background: rgba(198,108,77,0.05); border-radius: 3px; margin-bottom: 18px; display: none; }
  .err.show { display: block; }
</style>
</head>
<body>

<header>
  <div class="brand">
    <span class="name">Ventura<em>Claw</em></span>
    <span class="crumb">Admin · Services Leads</span>
  </div>
  <nav>
    <a href="/admin">Dashboard</a>
    <a href="/admin/users">Users</a>
    <a href="/admin/connectors">Connectors</a>
    <a href="/admin/approvals">Approvals</a>
    <a href="/admin/audit">Audit</a>
    <button class="theme-toggle" id="themeToggle" type="button" title="Toggle light/dark" aria-label="Toggle theme"><span class="moon">☾</span><span class="sun">☀︎</span></button>
  </nav>
</header>

<main>
  <h1>Services <em>Leads</em></h1>
  <div class="subhead"><b id="lead-count">…</b> total · live capture from /services hub</div>

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

  <div class="toolbar">
    <input type="search" id="filter" placeholder="filter by email, service, state, notes…" />
    <select id="period">
      <option value="all">all time</option>
      <option value="24h">last 24h</option>
      <option value="7d">last 7 days</option>
      <option value="30d">last 30 days</option>
    </select>
    <button class="refresh-btn" id="refresh">↻ Refresh</button>
    <span class="stats" id="stats"></span>
  </div>

  <div id="content"></div>
</main>

<script>
  let LEADS = [];
  const $ = id => document.getElementById(id);

  async function load() {
    $('err').classList.remove('show');
    try {
      const r = await fetch('/api/admin/services-leads', { credentials: 'include' });
      if (!r.ok) {
        const t = await r.text();
        throw new Error(`HTTP ${r.status}: ${t.slice(0, 120)}`);
      }
      const j = await r.json();
      LEADS = j.leads || [];
      $('lead-count').textContent = j.count.toLocaleString();
      render();
    } catch (e) {
      $('err').textContent = e.message;
      $('err').classList.add('show');
      $('content').innerHTML = '';
    }
  }

  function render() {
    const q = $('filter').value.trim().toLowerCase();
    const period = $('period').value;
    const cutoff = ({
      '24h': Date.now() - 24*3600*1000,
      '7d':  Date.now() - 7*24*3600*1000,
      '30d': Date.now() - 30*24*3600*1000,
      'all': 0,
    })[period];

    const filtered = LEADS.filter(l => {
      if (cutoff && new Date(l.at).getTime() < cutoff) return false;
      if (!q) return true;
      const blob = `${l.service} ${l.email} ${l.state||''} ${l.notes||''} ${l.phone||''}`.toLowerCase();
      return blob.includes(q);
    });

    $('stats').innerHTML = `<b>${filtered.length}</b>shown of <b>${LEADS.length}</b>loaded`;

    if (filtered.length === 0) {
      $('content').innerHTML = `<div class="empty">
        <span class="glyph">∅</span>
        <div class="msg">${LEADS.length === 0 ? 'No leads yet' : 'No leads match those filters'}</div>
        <div class="sub">${LEADS.length === 0 ? 'Once someone hits "Get Started →" on /services, leads land here.' : 'Try widening the period or clearing the search.'}</div>
      </div>`;
      return;
    }

    const now = Date.now();
    const isNew = at => (now - new Date(at).getTime()) < 24*3600*1000;
    const fmt = at => {
      const d = new Date(at);
      return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }) + ' ' + d.toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' });
    };
    const esc = s => (s || '').replace(/[<>&"]/g, c => ({'<':'&lt;','>':'&gt;','&':'&amp;','"':'&quot;'}[c]));

    $('content').innerHTML = `<table>
      <thead><tr>
        <th>When</th><th>Service</th><th>Email</th><th>Phone</th><th>State</th><th>Notes</th><th>Source</th>
      </tr></thead>
      <tbody>
        ${filtered.map(l => `<tr>
          <td class="at">
            ${isNew(l.at) ? '<span class="badge new">New</span>' : '<span class="badge old">·</span>'}<br>
            ${fmt(l.at)}
          </td>
          <td class="service">${esc(l.service)}</td>
          <td class="email"><a href="mailto:${esc(l.email)}?subject=Re:%20${encodeURIComponent(l.service)}%20%E2%80%94%20VenturaClaw">${esc(l.email)}</a></td>
          <td>${l.phone ? `<a href="tel:${esc(l.phone)}">${esc(l.phone)}</a>` : '<span style="color:var(--ink-faint)">—</span>'}</td>
          <td>${esc(l.state) || '<span style="color:var(--ink-faint)">—</span>'}</td>
          <td class="notes">${esc(l.notes) || '<span style="color:var(--ink-faint)">—</span>'}</td>
          <td class="id">${esc(l.id)}<br><span style="color:var(--ink-faint)">${esc(l.ip || '')}</span></td>
        </tr>`).join('')}
      </tbody>
    </table>`;
  }

  $('filter').addEventListener('input', render);
  $('period').addEventListener('change', render);
  $('refresh').addEventListener('click', load);

  // Theme toggle wiring (anti-flash already applied in <head>).
  document.getElementById('themeToggle').addEventListener('click', () => {
    const cur = document.documentElement.getAttribute('data-theme') === 'light' ? 'light' : 'dark';
    const next = cur === 'light' ? 'dark' : 'light';
    if (next === 'light') document.documentElement.setAttribute('data-theme', 'light');
    else document.documentElement.removeAttribute('data-theme');
    try { localStorage.setItem('vc-theme', next); } catch {}
  });

  load();
  setInterval(load, 30000); // auto-refresh every 30s
</script>

</body>
</html>