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