← 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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,'''); }
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>