@@ -274,19 +274,86 @@ function mkCheckbox(id, label, checked = true) {
274274 </label>` ;
275275}
276276
277- function mkRevealField ( id , placeholder , value = '' ) {
277+ function mkRevealField ( id , placeholder , value = '' , provider = '' ) {
278+ // When `provider` is set, render an extra "Test" button that fires a tiny
279+ // authenticated round-trip against that provider's API to verify the key
280+ // is reachable + valid before the user runs a real pipeline.
281+ const testBtn = provider
282+ ? `<button class="test-btn" id="${ id } -test" data-provider="${ provider } "
283+ data-target="${ id } " title="Test connection">Test</button>`
284+ : '' ;
285+ const status = provider
286+ ? `<span class="key-status" id="${ id } -status"></span>`
287+ : '' ;
278288 return `<div class="reveal-wrap">
279289 <input type="password" id="${ id } " class="input-text"
280290 placeholder="${ esc ( placeholder ) } " value="${ esc ( value ) } ">
281291 <button class="reveal-btn" onclick="toggleReveal('${ id } ')">👁</button>
282- </div>` ;
292+ ${ testBtn }
293+ </div>${ status } ` ;
283294}
284295
285296function toggleReveal ( id ) {
286297 const el = document . getElementById ( id ) ;
287298 if ( el ) el . type = el . type === 'password' ? 'text' : 'password' ;
288299}
289300
301+ async function testAllCredentials ( ) {
302+ // Fan out one test per non-empty field. Skips fields the user hasn't
303+ // filled in — testing an empty Anthropic key when the user only uses
304+ // OpenAI would just produce noise.
305+ const btn = document . getElementById ( 'test-all-keys-btn' ) ;
306+ if ( btn ) btn . disabled = true ;
307+ const buttons = document . querySelectorAll ( '.test-btn' ) ;
308+ const tests = [ ] ;
309+ for ( const b of buttons ) {
310+ const tid = b . dataset . target ;
311+ const v = ( document . getElementById ( tid ) ?. value || '' ) . trim ( ) ;
312+ if ( ! v ) continue ; // skip empty fields silently
313+ tests . push ( testCredentialField ( b ) ) ;
314+ }
315+ await Promise . all ( tests ) ;
316+ if ( btn ) btn . disabled = false ;
317+ }
318+
319+ async function testCredentialField ( btn ) {
320+ const provider = btn . dataset . provider ;
321+ const targetId = btn . dataset . target ;
322+ const input = document . getElementById ( targetId ) ;
323+ const status = document . getElementById ( `${ targetId } -status` ) ;
324+ if ( ! input || ! status ) return ;
325+ const key = ( input . value || '' ) . trim ( ) ;
326+ if ( ! key ) {
327+ status . textContent = '— enter a key first' ;
328+ status . className = 'key-status warn' ;
329+ return ;
330+ }
331+ // Canvas needs the URL too; pull it straight from the form so the user
332+ // doesn't have to Save All before testing.
333+ const extra = { } ;
334+ if ( provider === 'canvas' ) {
335+ extra . canvasUrl = ( document . getElementById ( 'cfg-canvas-url' ) ?. value || '' ) . trim ( ) ;
336+ }
337+ btn . disabled = true ;
338+ status . textContent = 'Testing…' ;
339+ status . className = 'key-status pending' ;
340+ try {
341+ const r = await window . api . testCredential ( provider , key , extra ) ;
342+ if ( r . ok ) {
343+ status . textContent = '✓ ' + r . message ;
344+ status . className = 'key-status ok' ;
345+ } else {
346+ status . textContent = '✗ ' + r . message ;
347+ status . className = 'key-status err' ;
348+ }
349+ } catch ( e ) {
350+ status . textContent = '✗ ' + ( e . message || e ) ;
351+ status . className = 'key-status err' ;
352+ } finally {
353+ btn . disabled = false ;
354+ }
355+ }
356+
290357// ── Run pipeline command ───────────────────────────────────────────────────────
291358function runCmd ( cmd , label = '' ) {
292359 if ( State . running ) {
@@ -1284,27 +1351,41 @@ async function loadSettingsData() {
12841351 keysEl . innerHTML = `
12851352 <div class="row">
12861353 <div class="col expand"><label class="label">Canvas Token</label>
1287- ${ mkRevealField ( 'cred-canvas' , 'Canvas API token' , creds . canvas ) } </div>
1354+ ${ mkRevealField ( 'cred-canvas' , 'Canvas API token' , creds . canvas , 'canvas' ) } </div>
12881355 </div>
12891356 <div class="row">
12901357 <div class="col expand"><label class="label">OpenAI API Key</label>
1291- ${ mkRevealField ( 'cred-openai' , 'sk-…' , creds . openai ) } </div>
1358+ ${ mkRevealField ( 'cred-openai' , 'sk-…' , creds . openai , 'openai' ) } </div>
12921359 <div class="col expand"><label class="label">Anthropic API Key</label>
1293- ${ mkRevealField ( 'cred-anthropic' , 'sk-ant-…' , creds . anthropic ) } </div>
1360+ ${ mkRevealField ( 'cred-anthropic' , 'sk-ant-…' , creds . anthropic , 'anthropic' ) } </div>
12941361 </div>
12951362 <div class="row">
12961363 <div class="col expand"><label class="label">Gemini API Key</label>
1297- ${ mkRevealField ( 'cred-gemini' , 'AIza…' , creds . gemini ) } </div>
1364+ ${ mkRevealField ( 'cred-gemini' , 'AIza…' , creds . gemini , 'gemini' ) } </div>
12981365 <div class="col expand"><label class="label">DeepSeek API Key</label>
1299- ${ mkRevealField ( 'cred-deepseek' , 'sk-…' , creds . deepseek ) } </div>
1366+ ${ mkRevealField ( 'cred-deepseek' , 'sk-…' , creds . deepseek , 'deepseek' ) } </div>
13001367 </div>
13011368 <div class="row">
13021369 <div class="col expand"><label class="label">xAI (Grok) API Key</label>
1303- ${ mkRevealField ( 'cred-grok' , 'xai-…' , creds . grok ) } </div>
1370+ ${ mkRevealField ( 'cred-grok' , 'xai-…' , creds . grok , 'grok' ) } </div>
13041371 <div class="col expand"><label class="label">Mistral API Key</label>
1305- ${ mkRevealField ( 'cred-mistral' , 'key…' , creds . mistral ) } </div>
1372+ ${ mkRevealField ( 'cred-mistral' , 'key…' , creds . mistral , 'mistral' ) } </div>
1373+ </div>
1374+ <div class="row">
1375+ <div class="col expand">
1376+ <button class="btn-secondary" id="test-all-keys-btn">Test all keys</button>
1377+ <span class="hint">Each "Test" button sends a single auth-only
1378+ request to the provider — no completion tokens are billed.</span>
1379+ </div>
13061380 </div>
13071381 ` ;
1382+ // Wire up per-field Test buttons + the bulk "Test all" button. Use
1383+ // event delegation so dynamically-rendered buttons work.
1384+ keysEl . addEventListener ( 'click' , ( e ) => {
1385+ const btn = e . target . closest ( '.test-btn' ) ;
1386+ if ( btn ) testCredentialField ( btn ) ;
1387+ } ) ;
1388+ document . getElementById ( 'test-all-keys-btn' ) ?. addEventListener ( 'click' , testAllCredentials ) ;
13081389 }
13091390
13101391 // Venv status
@@ -1391,6 +1472,13 @@ async function saveAllSettings() {
13911472
13921473 if ( btn ) btn . disabled = false ;
13931474
1475+ // Verify each API key the user just saved by firing a tiny auth-only
1476+ // round-trip per provider. Failures show inline next to the field; we
1477+ // don't block the save — the user might be deliberately offline.
1478+ if ( typeof testAllCredentials === 'function' ) {
1479+ testAllCredentials ( ) . catch ( ( ) => { } ) ;
1480+ }
1481+
13941482 if ( errors . length ) {
13951483 snack ( 'Errors: ' + errors . join ( '; ' ) , false ) ;
13961484 } else {
0 commit comments