// ========== CONNECT YOUR AI (DC-162) ========== // One button, one flow: click "🤖 Connect AI" → pick Claude/ChatGPT/generic // → DashCaddy mints a dedicated dk_ key + ready-to-paste MCP config. The // dashboard's own session does the signing-in; the AI assistant authenticates // with the key against POST /api/v1/mcp. Keys listed/revokeable here too. (function() { const errorHandler = new ErrorHandler(); injectModal('ai-connect-modal', `

🤖 Connect your AI assistant

DashCaddy speaks MCP — Claude, ChatGPT and other AI assistants can manage your homelab (deploy apps, check health, edit DNS) through one endpoint. Pick an assistant and DashCaddy generates the access key + config for it.
Choose your assistant:
Active AI connections
Loading…
`); const $ = (id) => document.getElementById(id); let info = null; function setStatus(msg) { $('aiconn-status').textContent = msg; } async function loadInfo() { try { const resp = await secureFetch('/api/v1/mcp/info'); if (!resp.ok) throw new Error(`HTTP ${resp.status}`); const body = await resp.json(); info = body; $('aiconn-endpoint').style.display = 'block'; $('aiconn-endpoint-url').textContent = body.endpoint; $('aiconn-tools').textContent = `${body.toolCount} tools · MCP ${body.protocolVersion} · DashCaddy v${body.serverVersion}`; } catch (e) { errorHandler.log(e, 'ai-connect-info'); setStatus('MCP endpoint unavailable on this build.'); } } async function loadConnections() { const listEl = $('aiconn-connections'); try { const resp = await secureFetch('/api/v1/mcp/connections'); const body = await resp.json().catch(() => ({})); const keys = (body && Array.isArray(body.connections)) ? body.connections : []; if (!keys.length) { listEl.innerHTML = '
No connections yet — choose an assistant above.
'; return; } listEl.innerHTML = keys.map(k => `
${escapeHtml(k.name)} · ${(k.scopes || []).join(',')} · since ${escapeHtml((k.createdAt || '').slice(0, 10))}${k.lastUsed ? ' · last used ' + escapeHtml(String(k.lastUsed).slice(0, 10)) : ''}
`).join(''); listEl.querySelectorAll('.aiconn-revoke').forEach(btn => btn.addEventListener('click', async () => { btn.disabled = true; try { const resp = await secureFetch(`/api/v1/auth/keys/${encodeURIComponent(btn.dataset.id)}`, { method: 'DELETE' }); if (resp.ok) { setStatus('Connection revoked.'); loadConnections(); } else { const b = await resp.json().catch(() => ({})); setStatus(b.error || 'Revoke failed.'); btn.disabled = false; } } catch (e) { setStatus('Revoke failed: ' + e.message); btn.disabled = false; } })); } catch (e) { listEl.innerHTML = '
Could not load connections.
'; } } async function createConnection(client) { setStatus(`Creating ${client} connection…`); try { const resp = await secureFetch('/api/v1/mcp/config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ client }), }); const body = await resp.json().catch(() => ({})); if (!resp.ok || !body.key) { setStatus(body.error || `Failed (HTTP ${resp.status})`); return; } $('aiconn-key').textContent = body.key; // rev2 config shape: httpConfig = {url, transport, headers, // mcpProtocolVersion} — the standard Streamable-HTTP connection card. const snippet = body.httpConfig ? JSON.stringify(body.httpConfig, null, 2) : ''; $('aiconn-snippet').textContent = snippet || '(see note below)'; $('aiconn-client-note').textContent = body.note || ''; $('aiconn-config-title').textContent = client === 'chatgpt' ? 'Access key (ChatGPT note below):' : 'Your access key (shown once):'; $('aiconn-step-pick').style.display = 'none'; $('aiconn-step-config').style.display = 'block'; setStatus(''); loadConnections(); } catch (e) { errorHandler.log(e, 'ai-connect-create'); setStatus('Failed: ' + e.message); } } function copyText(text, okMsg) { if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(text).then(() => setStatus(okMsg)).catch(() => setStatus('Copy failed — select manually.')); } else { setStatus('Clipboard unavailable — select manually.'); } } // ---- wiring ---- $('ai-connect-btn').addEventListener('click', () => { $('ai-connect-modal').style.display = 'flex'; $('aiconn-step-pick').style.display = 'block'; $('aiconn-step-config').style.display = 'none'; setStatus(''); loadInfo(); loadConnections(); }); $('aiconn-close').addEventListener('click', () => { $('ai-connect-modal').style.display = 'none'; }); $('aiconn-back').addEventListener('click', () => { $('aiconn-step-config').style.display = 'none'; $('aiconn-step-pick').style.display = 'block'; }); document.querySelectorAll('.aiconn-client-btn').forEach(btn => btn.addEventListener('click', () => createConnection(btn.dataset.client))); $('aiconn-copy-key').addEventListener('click', () => copyText($('aiconn-key').textContent, 'Key copied — paste it into your client.')); $('aiconn-copy-snippet').addEventListener('click', () => copyText($('aiconn-snippet').textContent, 'Config copied.')); $('aiconn-copy-endpoint').addEventListener('click', () => copyText(info ? info.endpoint : $('aiconn-endpoint-url').textContent, 'Endpoint copied.')); })();