← back to Ventura Claw
server/public/admin-connectors.html
193 lines
<!doctype html>
<html lang="en"><head>
<meta charset="utf-8" /><meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Connectors — VenturaClaw</title>
<link rel="stylesheet" href="/static/style.css" />
<style>
.tile { border: 1px solid var(--rule); transition: border-color 200ms ease, box-shadow 200ms ease; }
.tile.live { border-color: var(--good); box-shadow: 0 0 0 1px var(--good) inset; }
.tile.offline { border-color: var(--bad); box-shadow: 0 0 0 1px var(--bad) inset; }
.tile.mock { border-color: var(--rule); border-style: dashed; opacity: 0.55; }
.tile .stat-line { font-family: var(--mono); font-size: 9px; letter-spacing: .12em; text-transform: uppercase; margin-top: 6px; }
.tile.live .stat-line { color: var(--good); }
.tile.offline .stat-line { color: var(--bad); }
.tile.mock .stat-line { color: var(--ink-faint); }
.stats-bar { display: flex; gap: 14px; align-items: center; font-family: var(--mono); font-size: 10px; letter-spacing: .14em; text-transform: uppercase; }
.stat-pill { display: inline-flex; gap: 6px; align-items: center; padding: 5px 10px; border: 1px solid var(--rule); border-radius: 2px; }
.stat-pill .dot { width: 7px; height: 7px; border-radius: 50%; }
.stat-pill.live { border-color: var(--good); color: var(--good); }
.stat-pill.live .dot { background: var(--good); box-shadow: 0 0 6px var(--good); }
.stat-pill.offline { border-color: var(--bad); color: var(--bad); }
.stat-pill.offline .dot { background: var(--bad); }
.stat-pill.mock { color: var(--ink-mute); }
.stat-pill.mock .dot { background: var(--ink-faint); }
</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">Connectors</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="/admin" style="padding:8px 14px;color:var(--ink-soft)">Dashboard</a>
<a href="/admin/users" style="padding:8px 14px;color:var(--ink-soft)">Users</a>
<a href="/admin/connectors" style="padding:8px 14px;color:var(--gold);border-bottom:1px solid var(--gold)">Connectors</a>
<a href="/admin/oauth-setup" style="padding:8px 14px;color:var(--gold);background:var(--gold-soft);border:1px solid var(--gold);border-radius:2px">⚡ OAuth Setup</a>
<a href="/connections/import" style="padding:8px 14px;color:var(--gold);background:var(--gold-soft);border:1px solid var(--gold);border-radius:2px">↓ Import</a>
<a href="/admin/approvals" style="padding:8px 14px;color:var(--ink-soft)">Approvals</a>
<a href="/admin/audit" style="padding:8px 14px;color:var(--ink-soft)">Audit</a>
<a href="/chat" style="padding:8px 14px;color:var(--ink-soft)">Chat</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:1480px;margin:0 auto">
<div style="background:var(--gold-soft);border:1px solid var(--gold);border-left:3px solid var(--gold);border-radius:2px;padding:18px 22px;margin-bottom:22px;display:flex;gap:18px;align-items:center;flex-wrap:wrap">
<div style="flex:1;min-width:280px">
<div style="font-family:var(--serif);font-style:italic;font-size:22px;color:var(--ink);margin-bottom:4px">Bring your own tokens.</div>
<div style="font-family:var(--mono);font-size:11px;letter-spacing:.10em;color:var(--ink-soft);line-height:1.5">Paste your <code style="color:var(--gold)">~/.claude.json</code> mcpServers block or your <code style="color:var(--gold)">.env</code> file — connectors auto-light up. 42 keys mapped across 22 services.</div>
</div>
<a href="/connections/import" class="btn btn-primary" style="font-family:var(--mono)">↓ Import tokens →</a>
</div>
<div class="glass" style="padding:18px;margin-bottom:18px;display:flex;gap:14px;align-items:center;flex-wrap:wrap">
<input id="filter" placeholder="filter connectors…" style="flex:1;min-width:200px" />
<select id="cat-filter" style="min-width:160px"><option value="">all categories</option></select>
<button id="prime-btn" type="button" class="btn" style="font-family:var(--mono);font-size:10px;letter-spacing:.14em;text-transform:uppercase;padding:8px 14px;border:1px solid var(--gold);color:var(--gold);background:transparent;border-radius:2px;cursor:pointer" title="Re-fire the 5 preset cache primes — useful when Mac1 has evicted qwen3 and visitors are eating cold-loads">● Prime cache</button>
<span id="prime-status" role="status" aria-live="polite" style="font-family:var(--mono);font-size:10px;color:var(--ink-mute);min-width:80px"></span>
<select id="status-filter" style="min-width:140px">
<option value="">all status</option>
<option value="live">live API</option>
<option value="offline">not connected</option>
<option value="mock">mock only</option>
</select>
<div class="stats-bar">
<span class="stat-pill live"><span class="dot"></span>live <strong id="cnt-live">0</strong></span>
<span class="stat-pill offline"><span class="dot"></span>offline <strong id="cnt-off">0</strong></span>
<span class="stat-pill mock"><span class="dot"></span>mock <strong id="cnt-mock">0</strong></span>
<span style="color:var(--ink-mute)"><strong id="count">0</strong> shown / <strong id="total">0</strong> total</span>
</div>
</div>
<div class="glass" style="padding:18px"><div class="tile-grid" id="grid" role="region" aria-label="Connector grid" aria-live="polite"></div></div>
</main>
<div class="footer">VenturaClaw</div>
<script>
let CONNECTORS = [];
const HEALTH = {}; // { connector_id: { ok, reason, ... } }
function esc(s) { return String(s == null ? '' : s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,'''); }
function logoHTML(c) {
const tint = String(c.tint || '#888').replace(/[^#0-9a-fA-F]/g,'').slice(0,7);
const tintNoHash = tint.replace('#','');
const letter = esc((c.name || '?')[0]);
if (c.logo) {
const slug = String(c.logo).replace(/[^a-z0-9-]/gi,'');
const url = `https://cdn.simpleicons.org/${slug}/${tintNoHash}`;
return `<img class="logo" src="${esc(url)}" alt="${esc(c.name)}" loading="lazy" data-tint="${esc(tint)}" data-letter="${letter}" onerror="this.outerHTML='<div class="logo logo-fb" style="background:'+this.dataset.tint+'">'+this.dataset.letter+'</div>'.replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'\\'')">`;
}
return `<div class="logo logo-fb" style="background:${esc(tint)}">${letter}</div>`;
}
function statusOf(c) {
if (!c.real_impl) return "mock";
const h = HEALTH[c.id];
if (h && h.ok) return "live";
return "offline";
}
function statusLabel(c) {
const s = statusOf(c);
const h = HEALTH[c.id] || {};
if (s === "live") return `● connected · ${h.team || h.account_id || h.account || h.bot || h.user_id || h.friendly_name || "verified"}`;
if (s === "offline") return `○ not connected${h.reason ? ` · ${h.reason}` : ''}`;
return "○ mock — no real impl yet";
}
function draw() {
const q = filter.value.toLowerCase(), cat = catFilter.value, stat = statusFilter.value;
const list = CONNECTORS.filter(c =>
(!q || c.name.toLowerCase().includes(q)) &&
(!cat || c.category === cat) &&
(!stat || statusOf(c) === stat));
document.getElementById('count').textContent = list.length;
document.getElementById('total').textContent = CONNECTORS.length;
let live = 0, off = 0, mock = 0;
for (const c of CONNECTORS) { const s = statusOf(c); if (s === "live") live++; else if (s === "offline") off++; else mock++; }
document.getElementById('cnt-live').textContent = live;
document.getElementById('cnt-off').textContent = off;
document.getElementById('cnt-mock').textContent = mock;
document.getElementById('grid').innerHTML = list.map(c => {
const s = statusOf(c);
const docsHref = (typeof c.docs === 'string' && /^https?:\/\//.test(c.docs)) ? c.docs : '#';
return `<div class="tile ${s}">
<div class="head">${logoHTML(c)}<div><div class="cat">${esc(c.category)}</div><div class="name">${esc(c.name)}</div></div></div>
<div class="meta">${esc(c.triggers)}t · ${esc(c.actions)}a · ${esc(c.auth)}${c.sensitive?' · ⚠':''}</div>
<div class="stat-line">${esc(statusLabel(c))}</div>
<a href="${esc(docsHref)}" target="_blank" rel="noopener" style="font-size:10px;opacity:0.7">docs ↗</a>
</div>`;
}).join('');
}
const filter = document.getElementById('filter'),
catFilter = document.getElementById('cat-filter'),
statusFilter = document.getElementById('status-filter');
(async () => {
const d = await (await fetch('/api/connectors')).json();
CONNECTORS = d.connectors;
const cats = [...new Set(CONNECTORS.map(c=>c.category))];
catFilter.innerHTML += cats.map(c => `<option>${c}</option>`).join('');
draw();
// Single bulk health probe — server-side fanout + 30s TTL cache.
// Replaces the prior 56-parallel-fetch N+1 (Steve, perf-engineer audit 2026-05-05).
try {
const r = await fetch('/api/connectors/health-all');
if (r.ok) {
const { data } = await r.json();
Object.assign(HEALTH, data);
} else {
// Fallback: per-connector probes if the bulk endpoint fails for any reason
const realIds = CONNECTORS.filter(c => c.real_impl).map(c => c.id);
await Promise.all(realIds.map(async id => {
try { HEALTH[id] = await (await fetch(`/api/connectors/${id}/health`)).json(); }
catch { HEALTH[id] = { ok: false, reason: "probe failed" }; }
}));
}
} catch (e) {
console.error('health probe failed:', e);
}
draw();
})();
filter.addEventListener('input', draw);
catFilter.addEventListener('change', draw);
statusFilter.addEventListener('change', draw);
// Prime cache button — calls POST /api/admin/cache/prime, shows result inline.
document.getElementById('prime-btn').addEventListener('click', async (e) => {
const btn = e.currentTarget;
const status = document.getElementById('prime-status');
const orig = btn.textContent;
btn.disabled = true; btn.textContent = '● priming…';
status.textContent = ''; status.style.color = 'var(--ink-mute)';
try {
const r = await fetch('/api/admin/cache/prime', { method: 'POST' });
const d = await r.json();
if (r.ok) {
btn.textContent = `✓ primed ${d.primed}/${d.total}`;
status.textContent = `cache size: ${d.cache_size}`;
status.style.color = 'var(--good)';
} else {
btn.textContent = '✗ failed';
status.textContent = (d.error || 'unknown error').slice(0, 40);
status.style.color = 'var(--bad)';
}
} catch (err) {
btn.textContent = '✗ network error';
status.style.color = 'var(--bad)';
} finally {
setTimeout(() => { btn.textContent = orig; btn.disabled = false; }, 5000);
}
});
document.getElementById('logout').addEventListener('click', async e => {
e.preventDefault(); await fetch('/api/auth/logout', { method: 'POST' }); location.href = '/login';
});
</script>
</body></html>