← back to Ventura Claw

server/public/fill.html

147 lines

<!doctype html>
<html lang="en"><head>
<meta charset="utf-8" /><meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Fill in Tokens — VenturaClaw</title>
<link rel="stylesheet" href="/static/style.css" />
<style>
  .progress { display: flex; align-items: center; gap: 14px; padding: 18px; border: 1px solid var(--gold); border-radius: 4px; background: rgba(212,160,74,0.06); margin-bottom: 18px; }
  .progress-bar { flex: 1; height: 8px; background: rgba(244,241,234,0.06); border-radius: 999px; overflow: hidden; }
  .progress-fill { height: 100%; background: var(--gold); transition: width 600ms ease; }
  .progress-text { font-family: var(--mono); font-size: 11px; letter-spacing: .12em; color: var(--gold); white-space: nowrap; }
  .row { padding: 18px; border: 1px solid var(--rule); border-radius: 4px; margin-bottom: 12px; transition: all 400ms ease; display: grid; grid-template-columns: 48px 1fr; gap: 16px; align-items: start; }
  .row.done { border-color: var(--good); background: rgba(143,184,154,0.06); }
  .row.removing { opacity: 0; transform: translateX(-20px); max-height: 0; padding: 0; margin: 0; border: 0; overflow: hidden; }
  .logo { width: 40px; height: 40px; border-radius: 6px; background: rgba(244,241,234,0.92); padding: 6px; display: flex; align-items: center; justify-content: center; }
  .logo img { width: 100%; height: 100%; object-fit: contain; }
  .logo .fb { width: 100%; height: 100%; color: white; font-weight: 700; font-size: 16px; display: inline-flex; align-items: center; justify-content: center; border-radius: 4px; }
  .row h3 { font-family: var(--serif); font-weight: 500; font-size: 18px; margin: 0 0 4px; }
  .row .meta { font-family: var(--mono); font-size: 10px; letter-spacing: .12em; color: var(--ink-mute); margin-bottom: 8px; }
  .row .steps { font-size: 12px; color: var(--ink-soft); line-height: 1.55; margin-bottom: 12px; }
  .row .actions { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
  .row form { display: grid; grid-template-columns: 1fr auto auto; gap: 8px; align-items: stretch; margin-top: 10px; }
  .row form.multi { grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)) auto; }
  .row form input { font-family: var(--mono); font-size: 12px; padding: 8px 12px; }
  .row form button { white-space: nowrap; }
  .row .result { font-family: var(--mono); font-size: 11px; margin-top: 8px; min-height: 14px; }
  .row .result.ok  { color: var(--good); }
  .row .result.err { color: var(--bad); }
  .empty-state { text-align: center; padding: 60px 20px; }
  .empty-state .big { font-family: var(--serif); font-size: 28px; font-style: italic; color: var(--gold); margin-bottom: 8px; }
  .empty-state .small { font-family: var(--mono); font-size: 11px; letter-spacing: .12em; color: var(--ink-mute); }
</style>
</head><body>
<header class="topbar">
  <div class="brand"><div class="logo-dot"></div><div><div class="brand-name">Ventura<em style="font-style:italic;color:var(--gold)">Claw</em></div><div class="brand-sub">Fill in Tokens</div></div></div>
  <nav class="nav" style="display:flex;gap:6px;font-family:var(--mono);font-size:10px;letter-spacing:.14em;text-transform:uppercase">
    <a href="/chat"            style="padding:8px 14px;color:var(--ink-soft)">Chat</a>
    <a href="/connections"     style="padding:8px 14px;color:var(--ink-soft)">Connections</a>
    <a href="/fill"            style="padding:8px 14px;color:var(--gold);border-bottom:1px solid var(--gold)">Fill</a>
    <a href="/admin/connectors" style="padding:8px 14px;color:var(--ink-soft)">Admin</a>
    <a href="#" id="logout"     style="padding:8px 14px;color:var(--ink-soft)">Sign out</a>
  </nav>
</header>

<main style="padding:24px 28px;max-width:920px;margin:0 auto">
  <div class="progress">
    <div class="progress-bar"><div class="progress-fill" id="bar" style="width:0%"></div></div>
    <div class="progress-text" id="ptext">CHECKING…</div>
  </div>

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

<div class="footer">VenturaClaw · paste once, test on the spot. <a href="/connections" style="color:var(--gold)">advanced view →</a></div>

<script>
const esc = s => String(s == null ? "" : s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');

function logo(c) {
  if (c.logo) return `<div class="logo"><img src="https://cdn.simpleicons.org/${c.logo}/${(c.tint||'').replace('#','')}" alt="${esc(c.name)}" loading="lazy" onerror="this.outerHTML='<div class=\\'fb\\' style=\\'background:${esc(c.tint||'#999')}\\'>${esc(c.name[0])}</div>'"></div>`;
  return `<div class="logo"><div class="fb" style="background:${esc(c.tint||'#999')}">${esc(c.name[0])}</div></div>`;
}

async function load() {
  const meta = await (await fetch('/api/connectors/acquisition')).json();
  const conns = (meta.connectors || []).filter(c => c.real_impl);   // only real-impl connectors
  const total = conns.length;

  // Probe health for each filled connector — broken (red) tiles also need fill
  const probes = await Promise.all(conns.filter(c => c.filled).map(async c => {
    try { const h = await fetch(`/api/connectors/${c.id}/health`).then(r => r.json()); return { id: c.id, ok: h.ok }; }
    catch { return { id: c.id, ok: false }; }
  }));
  const okSet = new Set(probes.filter(p => p.ok).map(p => p.id));

  // Each connector: needs_fill = (!filled) OR (filled but health failed)
  const needs = conns.filter(c => !c.filled || !okSet.has(c.id));
  const done = total - needs.length;
  document.getElementById('bar').style.width = `${Math.round(done * 100 / total)}%`;
  document.getElementById('ptext').textContent = `${done} OF ${total} CONNECTED`;

  if (!needs.length) {
    document.getElementById('rows').innerHTML = `
      <div class="empty-state">
        <div class="big">all connected.</div>
        <div class="small">every real-impl connector has a working token. nothing left to fill.</div>
      </div>`;
    return;
  }

  document.getElementById('rows').innerHTML = needs.map(c => {
    const acq = c.acquisition;
    const fields = (acq?.fields || c.fields || []);
    const formCls = fields.length > 1 ? 'multi' : '';
    const fieldHtml = fields.map(f => `<input name="${esc(f.key)}" type="${f.type==='password'?'password':'text'}" placeholder="${esc(f.label || f.key)}" autocomplete="off" required="${f.required ? '' : ''}" />`).join('');
    return `<div class="row" data-id="${esc(c.id)}">
      ${logo(c)}
      <div>
        <h3>${esc(c.name)}</h3>
        <div class="meta">${esc(c.category)} · ${c.filled ? '<span style="color:var(--bad)">⚠ broken token</span>' : 'no token yet'}</div>
        ${acq?.get_key_steps ? `<div class="steps">${esc(acq.get_key_steps)}</div>` : ''}
        <div class="actions">
          ${acq?.get_key_url ? `<a href="${esc(acq.get_key_url)}" target="_blank" rel="noopener" class="btn">Get key ↗</a>` : ''}
          <a href="${esc(c.docs)}" target="_blank" rel="noopener" class="btn btn-ghost">Docs ↗</a>
        </div>
        <form data-form="${esc(c.id)}" class="${formCls}">
          ${fieldHtml}
          <button type="submit" class="btn btn-primary">Save & test</button>
        </form>
        <div class="result"></div>
      </div>
    </div>`;
  }).join('');

  document.querySelectorAll('form[data-form]').forEach(f => f.addEventListener('submit', async e => {
    e.preventDefault();
    const id = f.dataset.form;
    const row = f.closest('.row');
    const result = row.querySelector('.result');
    const body = {};
    for (const inp of f.querySelectorAll('input')) if (inp.value && inp.value.trim()) body[inp.name] = inp.value.trim();
    if (!Object.keys(body).length) { result.className = 'result err'; result.textContent = 'enter at least one field'; return; }
    result.className = 'result'; result.textContent = 'saving…';
    const save = await fetch(`/api/me/connections/${id}`, { method:'POST', headers:{'content-type':'application/json'}, body: JSON.stringify(body) });
    if (!save.ok) { const j = await save.json(); result.className = 'result err'; result.textContent = '✗ ' + (j.error || 'save failed'); return; }
    result.textContent = 'testing…';
    const h = await fetch(`/api/connectors/${id}/health`).then(r => r.json());
    if (h.ok) {
      result.className = 'result ok';
      result.textContent = '✓ live · ' + (h.team || h.account_id || h.user || h.account || h.user_id || h.endpoint || 'verified');
      row.classList.add('done');
      setTimeout(() => { row.classList.add('removing'); }, 1000);
      setTimeout(load, 1800); // refresh whole page state
    } else {
      result.className = 'result err';
      result.textContent = '✗ saved but health failed: ' + (h.reason || 'unknown');
    }
  }));
}

document.getElementById('logout').addEventListener('click', async e => {
  e.preventDefault(); await fetch('/api/auth/logout', { method: 'POST' }); location.href = '/login';
});

load();
</script>
</body></html>