← back to Ventura Claw

server/public/connections-import.html

226 lines

<!doctype html>
<html lang="en"><head>
<meta charset="utf-8" /><meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Import Connections — VenturaClaw</title>
<link rel="stylesheet" href="/static/style.css" />
<style>
.import-page { max-width: 880px; margin: 0 auto; padding: 28px; }
.import-card { background: var(--bg-elevated); border: 1px solid var(--rule-strong); border-radius: 4px; padding: 32px; }
.import-head h1 { font-family: var(--serif); font-weight: 500; font-size: 36px; letter-spacing: -.01em; line-height: 1; margin: 0 0 8px; }
.import-head h1 em { font-style: italic; color: var(--gold); }
.import-head .sub { font-family: var(--mono); font-size: 11px; letter-spacing: .14em; text-transform: uppercase; color: var(--ink-mute); margin-bottom: 24px; }
.import-lede { font-family: var(--serif); font-style: italic; font-size: 18px; color: var(--ink-soft); margin-bottom: 24px; line-height: 1.5; }
.format-row { display: flex; gap: 8px; margin-bottom: 12px; flex-wrap: wrap; }
.format-row label { font-family: var(--mono); font-size: 10px; letter-spacing: .12em; text-transform: uppercase; padding: 8px 14px; border: 1px solid var(--rule); cursor: pointer; transition: all 140ms ease; }
.format-row label:has(input:checked) { border-color: var(--gold); color: var(--gold); background: var(--gold-soft); }
.format-row input { display: none; }
textarea { width: 100%; min-height: 280px; font-family: var(--mono); font-size: 12px; line-height: 1.55; padding: 16px; resize: vertical; }
.help { background: rgba(212,160,74,0.06); border-left: 3px solid var(--gold); padding: 14px 18px; margin: 18px 0; border-radius: 2px; }
.help h3 { font-family: var(--serif); font-style: italic; font-size: 16px; font-weight: 500; color: var(--gold); margin: 0 0 8px; }
.help p { font-size: 13px; color: var(--ink-soft); line-height: 1.6; margin: 0 0 6px; }
.help code { font-family: var(--mono); background: rgba(244,241,234,0.06); padding: 1px 6px; border-radius: 2px; font-size: 11px; }
.actions { display: flex; gap: 10px; margin-top: 18px; align-items: center; }
.result { margin-top: 24px; border-top: 1px solid var(--rule); padding-top: 24px; }
.result h3 { font-family: var(--serif); font-style: italic; font-size: 22px; font-weight: 500; margin: 0 0 14px; }
.populated-row { display: grid; grid-template-columns: 1fr auto; align-items: baseline; padding: 10px 0; border-bottom: 1px dotted var(--rule); }
.populated-row .name { font-family: var(--serif); font-size: 18px; font-weight: 500; }
.populated-row .fields { font-family: var(--mono); font-size: 9px; letter-spacing: .12em; text-transform: uppercase; color: var(--good); }
.unmatched { margin-top: 14px; font-family: var(--mono); font-size: 10px; letter-spacing: .08em; color: var(--ink-mute); }
.unmatched b { color: var(--ink-soft); }
.privacy-note { font-family: var(--mono); font-size: 9px; letter-spacing: .14em; text-transform: uppercase; color: var(--ink-mute); margin-top: 18px; text-align: center; padding-top: 18px; border-top: 1px solid var(--rule); }
</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">Connected Operations</div>
    </div>
  </div>
  <div style="display:flex;gap:10px;align-items:center">
    <a href="/chat" class="btn btn-ghost">Chat</a>
    <a href="/connections" class="btn btn-ghost">Connections</a>
    <a href="/brand" class="btn btn-ghost">Brand</a>
    <a href="#" id="logout" class="btn btn-ghost">Sign out</a>
  </div>
</header>

<main class="import-page">
  <div class="import-card">
    <div class="import-head">
      <h1>Import <em>connections.</em></h1>
      <div class="sub">Bring your tokens from Claude Code, secrets-manager, or .env</div>
    </div>

    <p class="import-lede">Paste your <code style="font-family:var(--mono);font-size:14px;color:var(--gold)">~/.claude.json</code> mcpServers block, your secrets-manager <code style="font-family:var(--mono);font-size:14px;color:var(--gold)">.env</code> file, or any <code style="font-family:var(--mono);font-size:14px;color:var(--gold)">KEY=value</code> list. We'll auto-detect, parse, and wire matching connectors live.</p>

    <!-- DROP ZONE: drag .claude.json or .env onto the page → auto-import. Zero terminal. -->
    <div id="dropzone" style="border:2px dashed var(--gold);border-radius:4px;padding:36px 28px;margin-bottom:18px;text-align:center;background:rgba(212,160,74,0.04);cursor:pointer;transition:all 200ms ease">
      <div style="font-family:var(--serif);font-style:italic;font-size:32px;color:var(--gold);margin-bottom:8px;line-height:1">Drop your <em style="font-weight:500">file</em> here.</div>
      <div style="font-family:var(--mono);font-size:11px;letter-spacing:.10em;color:var(--ink-soft);line-height:1.7;margin-bottom:14px">
        Drag <code style="color:var(--gold)">~/.claude.json</code> or <code style="color:var(--gold)">.env</code> from Finder<br>
        <span style="color:var(--ink-mute)">— or —</span>
      </div>
      <button id="filepick-btn" class="btn btn-primary" style="font-size:11px;letter-spacing:.18em">📁 Choose file…</button>
      <input type="file" id="fileinput" accept=".json,.env,text/plain,application/json" style="display:none" />
      <div style="font-family:var(--mono);font-size:9px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-mute);margin-top:14px">All processing in your browser · 2 clicks total · no terminal</div>
    </div>

    <!-- Clipboard fallback (terminal-friendly path) -->
    <details style="margin-bottom:18px">
      <summary style="cursor:pointer;font-family:var(--mono);font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-mute);padding:8px 0">⚡ Or use clipboard (1 terminal cmd + 1 click) →</summary>
      <div style="background:rgba(212,160,74,0.04);border-left:2px solid var(--gold);padding:14px 18px;margin-top:8px;border-radius:2px">
        <div style="font-family:var(--mono);font-size:10px;letter-spacing:.10em;color:var(--ink-soft);line-height:1.7">
          On your Mac: <code style="background:rgba(0,0,0,0.4);color:var(--gold);padding:3px 8px;font-size:11px;border-radius:2px">cat ~/.claude.json | pbcopy</code><br>
          Then click: <button id="oneclick-btn" class="btn btn-primary" style="font-size:10px;letter-spacing:.16em;margin-top:6px">⚡ Read clipboard</button>
        </div>
      </div>
    </details>

    <div class="format-row">
      <label><input type="radio" name="fmt" value="auto" checked> Auto-detect</label>
      <label><input type="radio" name="fmt" value="claude-json"> ~/.claude.json (MCP)</label>
      <label><input type="radio" name="fmt" value="env"> .env (KEY=value)</label>
    </div>

    <textarea id="content" placeholder='Paste here. Examples:

  STRIPE_SECRET_KEY=sk_live_...
  SHOPIFY_ACCESS_TOKEN=shpat_...
  SLACK_BOT_TOKEN=xoxb-...

— or —

  {
    "mcpServers": {
      "stripe": { "command": "npx", "args": ["..."], "env": { "STRIPE_API_KEY": "sk_..." } },
      "shopify": { "env": { "SHOPIFY_ACCESS_TOKEN": "shpat_..." } }
    }
  }'></textarea>

    <div class="help">
      <h3>Where to find these</h3>
      <p><b style="color:var(--ink)">Claude Code config</b> — <code>cat ~/.claude.json</code> on your Mac. Copy the entire <code>mcpServers</code> block (or the whole file) and paste here.</p>
      <p><b style="color:var(--ink)">Secrets manager</b> — <code>cat ~/Projects/secrets-manager/.env</code></p>
      <p><b style="color:var(--ink)">Anything else</b> — paste any list of <code>KEY=value</code> pairs.</p>
    </div>

    <div class="actions">
      <button id="import-btn" class="btn btn-primary">Import</button>
      <span id="status" style="font-family:var(--mono);font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:var(--ink-mute)"></span>
    </div>

    <div id="result" class="result" style="display:none"></div>

    <p class="privacy-note">All values stored per-user · server-side · encrypted at rest · never logged</p>
  </div>
</main>

<script>
const btn = document.getElementById('import-btn');
const ta = document.getElementById('content');
const status = document.getElementById('status');
const resultEl = document.getElementById('result');

// ── Drag-and-drop + file picker (the 2-click path, zero terminal)
const dz = document.getElementById('dropzone');
const fi = document.getElementById('fileinput');
async function ingestFile(file) {
  if (!file) return;
  const text = await file.text();
  ta.value = text;
  document.querySelector('input[name="fmt"][value="auto"]').checked = true;
  status.textContent = `read ${file.name} (${(file.size/1024).toFixed(1)} KB) — importing…`;
  btn.click();
}
dz.addEventListener('click', e => { if (e.target.tagName !== 'BUTTON') fi.click(); });
document.getElementById('filepick-btn').addEventListener('click', e => { e.stopPropagation(); fi.click(); });
fi.addEventListener('change', e => ingestFile(e.target.files[0]));
['dragenter','dragover'].forEach(ev => dz.addEventListener(ev, e => {
  e.preventDefault(); dz.style.background = 'rgba(212,160,74,0.14)'; dz.style.transform = 'scale(1.01)';
}));
['dragleave','drop'].forEach(ev => dz.addEventListener(ev, e => {
  e.preventDefault(); dz.style.background = 'rgba(212,160,74,0.04)'; dz.style.transform = 'none';
}));
dz.addEventListener('drop', e => { e.preventDefault(); ingestFile(e.dataTransfer.files[0]); });

// One-click connect: read clipboard, populate textarea, auto-submit
document.getElementById('oneclick-btn').addEventListener('click', async () => {
  const oc = document.getElementById('oneclick-btn');
  oc.disabled = true; oc.textContent = '⚡ reading clipboard…';
  try {
    const text = await navigator.clipboard.readText();
    if (!text || !text.trim()) {
      oc.textContent = 'clipboard empty — copy your file first';
      setTimeout(() => { oc.textContent = '⚡ One-click connect →'; oc.disabled = false; }, 2400);
      return;
    }
    ta.value = text;
    document.querySelector('input[name="fmt"][value="auto"]').checked = true;
    oc.textContent = '⚡ importing…';
    btn.click();
  } catch (e) {
    oc.textContent = 'clipboard blocked — paste manually below';
    setTimeout(() => { oc.textContent = '⚡ One-click connect →'; oc.disabled = false; }, 2800);
  }
  setTimeout(() => { oc.textContent = '⚡ One-click connect →'; oc.disabled = false; }, 4000);
});

function esc(s) { return String(s == null ? '' : s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;'); }

btn.addEventListener('click', async () => {
  const content = ta.value.trim();
  if (!content) { status.textContent = 'paste something first'; return; }
  const format = document.querySelector('input[name="fmt"]:checked').value;
  btn.disabled = true; status.textContent = 'parsing…'; resultEl.style.display = 'none';
  try {
    const r = await fetch('/api/me/connections/import', {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({ content, format }),
    });
    const d = await r.json();
    if (!r.ok) { status.textContent = 'error: ' + esc(d.error || 'unknown'); btn.disabled = false; return; }
    status.textContent = `${d.populated.length} connector${d.populated.length === 1 ? '' : 's'} imported`;
    if (d.populated.length === 0) {
      resultEl.innerHTML = `
        <p style="font-family:var(--mono);font-size:12px;color:var(--ink-mute);line-height:1.6">
          No connectors matched the keys in your paste.
          <a href="/admin/connectors" style="color:var(--gold)">View the connector list</a>
          to see which key names are expected.
        </p>`;
    } else {
      resultEl.innerHTML = `
        <h3>Imported · <em style="font-style:italic;color:var(--gold)">${d.populated.length}</em> connector${d.populated.length === 1 ? '' : 's'}</h3>
        <div style="font-family:var(--mono);font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:var(--ink-mute);margin-bottom:12px">
          Format: ${esc(d.format)} · Saw ${Number(d.envKeysSeen) || 0} env var${d.envKeysSeen === 1 ? '' : 's'}${d.serverCount ? ' · ' + (Number(d.serverCount) || 0) + ' MCP server' + (d.serverCount === 1 ? '' : 's') : ''}
        </div>
        ${d.populated.map(p => `
          <div class="populated-row">
            <span class="name">${esc(p.connector)}</span>
            <span class="fields">${(p.fields || []).map(esc).join(' · ')}</span>
          </div>`).join('')}
        ${d.unmatched && d.unmatched.length > 0 ? `
          <div class="unmatched">
            <b>${d.unmatched.length} env var${d.unmatched.length === 1 ? '' : 's'} not yet mapped:</b>
            ${d.unmatched.slice(0, 12).map(k => `<code style="font-family:var(--mono);font-size:9px">${esc(k)}</code>`).join(', ')}
            ${d.unmatched.length > 12 ? '…' : ''}
          </div>` : ''}
        <div style="margin-top:18px"><a href="/connections" class="btn btn-primary" style="font-family:var(--mono)">View connections →</a></div>
      `;
    }
    resultEl.style.display = 'block';
    ta.value = '';
  } catch (e) {
    status.textContent = 'network error';
  }
  btn.disabled = false;
});

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