[object Object]

← back to Ventura Claw

Wizard: Test button — saves + validates against connector health, auto-advances on ✓

437cf54637a3ad7023c21b96fe1d32c8ba534b72 · 2026-05-06 16:47:20 -0700 · SteveStudio2

- New 'Test' button next to 'Save & Next →' chains save → /api/connectors/:id/health
- Inline result panel shows: ok (green, auto-advances 1.2s), 'no test handler' for
  catalog-only (yellow, stays put), error reason (red, stays put for fixing)
- 'Save & Next →' still works as fast-path skip-validation
- Test-result resets each step

Files touched

Diff

commit 437cf54637a3ad7023c21b96fe1d32c8ba534b72
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Wed May 6 16:47:20 2026 -0700

    Wizard: Test button — saves + validates against connector health, auto-advances on ✓
    
    - New 'Test' button next to 'Save & Next →' chains save → /api/connectors/:id/health
    - Inline result panel shows: ok (green, auto-advances 1.2s), 'no test handler' for
      catalog-only (yellow, stays put), error reason (red, stays put for fixing)
    - 'Save & Next →' still works as fast-path skip-validation
    - Test-result resets each step
---
 server/public/admin-wizard.html | 87 +++++++++++++++++++++++++++++++++++------
 1 file changed, 75 insertions(+), 12 deletions(-)

diff --git a/server/public/admin-wizard.html b/server/public/admin-wizard.html
index 40aae97..309f779 100644
--- a/server/public/admin-wizard.html
+++ b/server/public/admin-wizard.html
@@ -66,6 +66,19 @@
   .toast.ok  { background:rgba(143,184,154,0.18); border:1px solid var(--good); color:var(--good); }
   .toast.bad { background:rgba(200,122,110,0.18); border:1px solid var(--bad); color:var(--bad); }
 
+  /* Test-result inline panel (between fields and actions) */
+  .test-result {
+    margin: 4px 0 14px 0; padding: 10px 14px; border-radius: 3px;
+    font-family: var(--mono); font-size: 11px; letter-spacing: 0.04em;
+    display: none; line-height: 1.5; word-break: break-word;
+  }
+  .test-result.ok      { background: rgba(143,184,154,0.10); border: 1px solid var(--good);   color: var(--good); }
+  .test-result.skip    { background: rgba(212,160,74,0.06);  border: 1px solid var(--ink-faint); color: var(--ink-mute); }
+  .test-result.bad     { background: rgba(200,122,110,0.12); border: 1px solid var(--bad);    color: var(--bad); }
+  .test-result.testing { background: var(--bg-3); border: 1px solid var(--rule); color: var(--ink-mute); }
+  .test-result strong { font-weight: 600; }
+  .btn[disabled] { opacity: 0.5; cursor: not-allowed; }
+
   .done-card { text-align:center; padding:48px 32px; }
   .done-card .checkmark { font-size:64px; color:var(--good); margin-bottom:16px; }
   .done-card .stat { font-family:var(--serif); font-size:36px; color:var(--gold); margin:18px 0 8px 0; }
@@ -91,9 +104,11 @@
     <div class="question" id="question"></div>
     <div id="fields"></div>
     <div class="help-link" id="help-link"></div>
+    <div class="test-result" id="test-result"></div>
     <div class="actions">
       <button class="btn btn-skip" id="btn-skip">Skip</button>
       <button class="btn btn-vault" id="btn-vault" title="Try to find this key in the secrets-manager vault">🔍 Search Vault</button>
+      <button class="btn" id="btn-test" title="Save then validate against the provider's API">Test</button>
       <button class="btn btn-primary" id="btn-save">Save & Next →</button>
     </div>
   </div>
@@ -133,6 +148,8 @@ async function load() {
 function render() {
   const c = allConnectors[cursor];
   if (!c) return showDone(totalCatalog - allConnectors.length + cursor, totalCatalog);
+  // Reset test-result panel each step
+  const tr = $('#test-result'); if (tr) { tr.style.display = 'none'; tr.className = 'test-result'; tr.innerHTML = ''; }
   $('#step-tag').textContent = 'STEP ' + (cursor + 1) + ' OF ' + allConnectors.length;
   $('#conn-name').innerHTML = esc(c.name).replace(/\s+(\w+)$/, ' <em>$1</em>');
   $('#conn-category').textContent = c.category || '—';
@@ -186,29 +203,75 @@ $('#btn-skip').addEventListener('click', () => {
   render();
 });
 
-$('#btn-save').addEventListener('click', async () => {
-  const c = allConnectors[cursor];
+function collectFields(c) {
   const body = {};
   let any = false;
   for (const f of c.fields || []) {
     const el = $('#f-' + f.key);
     if (el && el.value.trim()) { body[f.key] = el.value.trim(); any = true; }
   }
-  if (!any) {
-    toast('Paste a key or hit Skip', true);
-    return;
-  }
+  return { body, any };
+}
+
+function setTestResult(state, html) {
+  const el = $('#test-result');
+  el.className = 'test-result ' + state;
+  el.innerHTML = html;
+  el.style.display = 'block';
+}
+
+async function saveCurrent() {
+  const c = allConnectors[cursor];
+  const { body, any } = collectFields(c);
+  if (!any) { toast('Paste a key or hit Skip', true); return null; }
   const r = await fetch('/api/me/connections/' + encodeURIComponent(c.id), {
     method: 'POST', headers: { 'Content-Type': 'application/json' },
     body: JSON.stringify(body),
   });
   const j = await r.json();
-  if (j.ok) {
-    toast('✓ ' + c.name + ' connected');
-    cursor++;
-    render();
-  } else {
-    toast('✗ ' + (j.error || 'failed'), true);
+  if (!j.ok) {
+    toast('✗ ' + (j.error || 'save failed'), true);
+    return null;
+  }
+  return c;
+}
+
+$('#btn-save').addEventListener('click', async () => {
+  const saved = await saveCurrent();
+  if (!saved) return;
+  toast('✓ ' + saved.name + ' saved');
+  cursor++;
+  render();
+});
+
+$('#btn-test').addEventListener('click', async () => {
+  const c = allConnectors[cursor];
+  $('#btn-test').disabled = true; $('#btn-save').disabled = true;
+  setTestResult('testing', 'Saving + testing against ' + esc(c.name) + '…');
+  const saved = await saveCurrent();
+  if (!saved) {
+    setTestResult('bad', '<strong>Save failed.</strong> Fix above and try again.');
+    $('#btn-test').disabled = false; $('#btn-save').disabled = false;
+    return;
+  }
+  // Now hit the health endpoint
+  try {
+    const r = await fetch('/api/connectors/' + encodeURIComponent(c.id) + '/health');
+    const h = await r.json();
+    if (h.ok) {
+      setTestResult('ok', '<strong>✓ ' + esc(c.name) + ' authenticates cleanly.</strong> ' + esc(h.label || h.account || h.email || ''));
+      // Auto-advance after 1.2s on success
+      setTimeout(() => { cursor++; render(); $('#btn-test').disabled = false; $('#btn-save').disabled = false; }, 1200);
+    } else if (h.reason === 'not_implemented') {
+      setTestResult('skip', '<strong>Saved.</strong> No test handler for ' + esc(c.name) + ' yet — credentials stored, will be used once a handler is wired.');
+      $('#btn-test').disabled = false; $('#btn-save').disabled = false;
+    } else {
+      setTestResult('bad', '<strong>✗ ' + esc(c.name) + ' rejected.</strong> ' + esc(h.reason || h.error || 'unknown') + (h.hint ? ' · ' + esc(h.hint) : ''));
+      $('#btn-test').disabled = false; $('#btn-save').disabled = false;
+    }
+  } catch (e) {
+    setTestResult('bad', '<strong>✗ Test crashed.</strong> ' + esc(e.message));
+    $('#btn-test').disabled = false; $('#btn-save').disabled = false;
   }
 });
 

← 7334fa7 Connector wizard: one-question-at-a-time onboarding at /admi  ·  back to Ventura Claw  ·  Wizard: bulk health strip — see all saved connectors auth st 1ad784f →