From 6ecfa1283b90098a181fd2071376e47a80a3adeb Mon Sep 17 00:00:00 2001 From: Jason Date: Wed, 13 May 2026 08:54:50 +0200 Subject: [PATCH 1/3] [unit: providers] Slice 8: Provider test frontend (TestButton) --- frontend/src/lib/api/providers.ts | 14 +++++++- frontend/src/lib/api/types.ts | 6 ++++ .../components/providers/ProviderCard.svelte | 4 +++ .../components/providers/TestButton.svelte | 28 +++++++++++++++ .../providers/TestButtonState.svelte.ts | 36 +++++++++++++++++++ 5 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 frontend/src/lib/components/providers/TestButton.svelte create mode 100644 frontend/src/lib/components/providers/TestButtonState.svelte.ts diff --git a/frontend/src/lib/api/providers.ts b/frontend/src/lib/api/providers.ts index daa6c968..49299918 100644 --- a/frontend/src/lib/api/providers.ts +++ b/frontend/src/lib/api/providers.ts @@ -1,5 +1,10 @@ import { apiClient } from './client'; -import type { ProviderCreateRequest, ProviderResponse, ProviderUpdateRequest } from './types'; +import type { + ProviderCreateRequest, + ProviderResponse, + ProviderTestResult, + ProviderUpdateRequest +} from './types'; export async function listProviders(): Promise { return apiClient.request({ @@ -15,3 +20,10 @@ export async function createProvider(data: ProviderCreateRequest): Promise { return apiClient.request({ method: 'PUT', path: `/providers/${id}`, body: data }); } + +export async function testProvider(id: string): Promise { + return apiClient.request({ + method: 'POST', + path: `/providers/${id}/test` + }); +} diff --git a/frontend/src/lib/api/types.ts b/frontend/src/lib/api/types.ts index 24791f99..1d0593f2 100644 --- a/frontend/src/lib/api/types.ts +++ b/frontend/src/lib/api/types.ts @@ -221,6 +221,12 @@ export interface SystemHealthResponse { } // --- Providers --- +export interface ProviderTestResult { + response_text: string; + model: string; + duration_ms: number; +} + export interface ProviderResponse { id: string; name: string; diff --git a/frontend/src/lib/components/providers/ProviderCard.svelte b/frontend/src/lib/components/providers/ProviderCard.svelte index 90568b5f..abbbc9f5 100644 --- a/frontend/src/lib/components/providers/ProviderCard.svelte +++ b/frontend/src/lib/components/providers/ProviderCard.svelte @@ -4,6 +4,7 @@ import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '$lib/components/ui/card'; import { Badge } from '$lib/components/ui/badge'; import { Globe, Key, Pencil } from 'lucide-svelte'; + import TestButton from './TestButton.svelte'; let { provider, @@ -44,5 +45,8 @@ {/if} +
+ +
diff --git a/frontend/src/lib/components/providers/TestButton.svelte b/frontend/src/lib/components/providers/TestButton.svelte new file mode 100644 index 00000000..ae7bd4ab --- /dev/null +++ b/frontend/src/lib/components/providers/TestButton.svelte @@ -0,0 +1,28 @@ + + +
+ {#if testState.state === 'idle'} + + {:else if testState.state === 'testing'} + + Testing... + {:else if testState.state === 'success' && testState.result} + + Working + {testState.result.model} + {testState.result.duration_ms}ms + + {:else if testState.state === 'error'} + + {testState.errorMessage} + + {/if} +
diff --git a/frontend/src/lib/components/providers/TestButtonState.svelte.ts b/frontend/src/lib/components/providers/TestButtonState.svelte.ts new file mode 100644 index 00000000..fdd5e3d8 --- /dev/null +++ b/frontend/src/lib/components/providers/TestButtonState.svelte.ts @@ -0,0 +1,36 @@ +import { testProvider } from '$lib/api/providers'; +import type { ProviderTestResult } from '$lib/api/types'; + +export type TestState = 'idle' | 'testing' | 'success' | 'error'; + +export function createTestButtonState(getProviderId: () => string) { + let state = $state('idle'); + let result = $state(null); + let errorMessage = $state(''); + + async function handleTest() { + state = 'testing'; + result = null; + errorMessage = ''; + try { + result = await testProvider(getProviderId()); + state = 'success'; + } catch (err) { + errorMessage = err instanceof Error ? err.message : 'Test failed'; + state = 'error'; + } + } + + return { + get state() { + return state; + }, + get result() { + return result; + }, + get errorMessage() { + return errorMessage; + }, + handleTest + }; +} From c6be866905e59d2858e07ee9a0a09221cc623bc8 Mon Sep 17 00:00:00 2001 From: Jason Date: Wed, 13 May 2026 17:09:49 +0200 Subject: [PATCH 2/3] [unit: providers] Inline test step after provider creation --- .../components/providers/ProviderForm.svelte | 40 ++++++++++++++++--- .../src/routes/(app)/providers/+page.svelte | 9 +++-- 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/frontend/src/lib/components/providers/ProviderForm.svelte b/frontend/src/lib/components/providers/ProviderForm.svelte index ddb5b6b0..65225a6d 100644 --- a/frontend/src/lib/components/providers/ProviderForm.svelte +++ b/frontend/src/lib/components/providers/ProviderForm.svelte @@ -4,6 +4,8 @@ import { Label } from '$lib/components/ui/label'; import { Select, SelectOption } from '$lib/components/ui/select'; import { Button } from '$lib/components/ui/button'; + import TestButton from './TestButton.svelte'; + import { CircleCheck } from 'lucide-svelte'; import type { ProviderCreateRequest, ProviderResponse, ProviderUpdateRequest } from '$lib/api/types'; const PROVIDER_TYPES = [ @@ -52,7 +54,7 @@ }: { open?: boolean; provider?: ProviderResponse; - onsave: (data: ProviderCreateRequest | ProviderUpdateRequest) => Promise; + onsave: (data: ProviderCreateRequest | ProviderUpdateRequest) => Promise; } = $props(); let name = $state(''); @@ -62,6 +64,10 @@ let isSubmitting = $state(false); let errors = $state>({}); + // Test step state + let step = $state<'form' | 'test'>('form'); + let createdProvider = $state(null); + // Config field state let timeoutMs = $state(''); let extraHeaders = $state<{ name: string; value: string }[]>([]); @@ -89,6 +95,8 @@ region = ''; errors = {}; isSubmitting = false; + step = 'form'; + createdProvider = null; } function populateFromProvider(p: ProviderResponse) { @@ -207,6 +215,7 @@ updateData.config_json = {}; } await onsave(updateData); + open = false; } else { const createData: ProviderCreateRequest = { name: name.trim(), @@ -217,9 +226,12 @@ if (Object.keys(newConfig).length > 0) { createData.config_json = newConfig; } - await onsave(createData); + const result = await onsave(createData); + if (result) { + createdProvider = result; + step = 'test'; + } } - open = false; } catch (err) { errors.form = err instanceof Error ? err.message : 'Save failed'; } finally { @@ -242,9 +254,26 @@
-

{title}

+ {#if step === 'test' && createdProvider} +
+ +

Provider Created

+
+

+ {createdProvider.name} + ({createdProvider.provider_type}) +

+
+ +
+
+ + +
+ {:else} +

{title}

-
+ {#if errors.form}

{errors.form}

{/if} @@ -408,5 +437,6 @@
+ {/if}
diff --git a/frontend/src/routes/(app)/providers/+page.svelte b/frontend/src/routes/(app)/providers/+page.svelte index 131b7786..f07e670a 100644 --- a/frontend/src/routes/(app)/providers/+page.svelte +++ b/frontend/src/routes/(app)/providers/+page.svelte @@ -36,13 +36,16 @@ showForm = true; } - async function handleSave(data: ProviderCreateRequest | ProviderUpdateRequest) { + async function handleSave(data: ProviderCreateRequest | ProviderUpdateRequest): Promise { if (editingProvider) { await updateProvider(editingProvider.id, data as ProviderUpdateRequest); + showForm = false; + await loadProviders(); } else { - await createProvider(data as ProviderCreateRequest); + const created = await createProvider(data as ProviderCreateRequest); + await loadProviders(); + return created; } - await loadProviders(); } onMount(() => { From ac7d68c92e0b62bedd8799e3a307c3f12cadd54f Mon Sep 17 00:00:00 2001 From: Jason Date: Wed, 13 May 2026 17:22:21 +0200 Subject: [PATCH 3/3] [unit: providers] Gate provider creation behind test step --- frontend/src/lib/api/providers.ts | 4 + .../components/providers/ProviderForm.svelte | 93 +++++++++++-------- .../src/routes/(app)/providers/+page.svelte | 5 +- 3 files changed, 59 insertions(+), 43 deletions(-) diff --git a/frontend/src/lib/api/providers.ts b/frontend/src/lib/api/providers.ts index 49299918..06134895 100644 --- a/frontend/src/lib/api/providers.ts +++ b/frontend/src/lib/api/providers.ts @@ -27,3 +27,7 @@ export async function testProvider(id: string): Promise { path: `/providers/${id}/test` }); } + +export async function deleteProvider(id: string): Promise { + return apiClient.request({ method: 'DELETE', path: `/providers/${id}` }); +} diff --git a/frontend/src/lib/components/providers/ProviderForm.svelte b/frontend/src/lib/components/providers/ProviderForm.svelte index 65225a6d..098860a8 100644 --- a/frontend/src/lib/components/providers/ProviderForm.svelte +++ b/frontend/src/lib/components/providers/ProviderForm.svelte @@ -4,9 +4,9 @@ import { Label } from '$lib/components/ui/label'; import { Select, SelectOption } from '$lib/components/ui/select'; import { Button } from '$lib/components/ui/button'; - import TestButton from './TestButton.svelte'; - import { CircleCheck } from 'lucide-svelte'; - import type { ProviderCreateRequest, ProviderResponse, ProviderUpdateRequest } from '$lib/api/types'; + import { CircleX } from 'lucide-svelte'; + import type { ProviderCreateRequest, ProviderResponse, ProviderTestResult, ProviderUpdateRequest } from '$lib/api/types'; + import { testProvider, deleteProvider } from '$lib/api/providers'; const PROVIDER_TYPES = [ 'openai', 'anthropic', 'google', 'azure', 'bedrock', @@ -54,7 +54,7 @@ }: { open?: boolean; provider?: ProviderResponse; - onsave: (data: ProviderCreateRequest | ProviderUpdateRequest) => Promise; + onsave: (data: ProviderCreateRequest | ProviderUpdateRequest) => Promise; } = $props(); let name = $state(''); @@ -64,9 +64,10 @@ let isSubmitting = $state(false); let errors = $state>({}); - // Test step state - let step = $state<'form' | 'test'>('form'); - let createdProvider = $state(null); + // Test state (create mode only) + let testResult = $state(null); + let testError = $state(null); + let isTesting = $state(false); // Config field state let timeoutMs = $state(''); @@ -95,8 +96,9 @@ region = ''; errors = {}; isSubmitting = false; - step = 'form'; - createdProvider = null; + testResult = null; + testError = null; + isTesting = false; } function populateFromProvider(p: ProviderResponse) { @@ -201,16 +203,17 @@ e.preventDefault(); if (!validate()) return; + testError = null; isSubmitting = true; try { - const newConfig = buildConfigJson(); + const configJson = buildConfigJson(); if (isEdit && provider) { const updateData: ProviderUpdateRequest = {}; if (name.trim() !== provider.name) updateData.name = name.trim(); if (baseUrl.trim() !== provider.base_url) updateData.base_url = baseUrl.trim(); if (apiKey.trim()) updateData.api_key = apiKey.trim(); - if (Object.keys(newConfig).length > 0) { - updateData.config_json = newConfig; + if (Object.keys(configJson).length > 0) { + updateData.config_json = configJson; } else if (Object.keys(provider.config_json).length > 0) { updateData.config_json = {}; } @@ -223,14 +226,29 @@ base_url: baseUrl.trim(), api_key: apiKey.trim() }; - if (Object.keys(newConfig).length > 0) { - createData.config_json = newConfig; + if (Object.keys(configJson).length > 0) { + createData.config_json = configJson; } - const result = await onsave(createData); - if (result) { - createdProvider = result; - step = 'test'; + const created = await onsave(createData); + isSubmitting = false; + isTesting = true; + try { + testResult = await testProvider(created.id); + // Test passed — close dialog + open = false; + } catch (err) { + const msg = err instanceof Error ? err.message : 'Test failed'; + testError = msg; + // Rollback: delete the provider + try { + await deleteProvider(created.id); + } catch { + // Silently ignore delete failure + } + } finally { + isTesting = false; } + return; } } catch (err) { errors.form = err instanceof Error ? err.message : 'Save failed'; @@ -254,30 +272,20 @@
- {#if step === 'test' && createdProvider} -
- -

Provider Created

-
-

- {createdProvider.name} - ({createdProvider.provider_type}) -

-
- -
-
- - -
- {:else} -

{title}

+

{title}

-
+ {#if errors.form}

{errors.form}

{/if} + {#if testError} +
+ + Connection failed: {testError}. The provider has been removed. +
+ {/if} +
-
- {/if}
diff --git a/frontend/src/routes/(app)/providers/+page.svelte b/frontend/src/routes/(app)/providers/+page.svelte index f07e670a..89411f97 100644 --- a/frontend/src/routes/(app)/providers/+page.svelte +++ b/frontend/src/routes/(app)/providers/+page.svelte @@ -36,11 +36,12 @@ showForm = true; } - async function handleSave(data: ProviderCreateRequest | ProviderUpdateRequest): Promise { + async function handleSave(data: ProviderCreateRequest | ProviderUpdateRequest): Promise { if (editingProvider) { - await updateProvider(editingProvider.id, data as ProviderUpdateRequest); + const updated = await updateProvider(editingProvider.id, data as ProviderUpdateRequest); showForm = false; await loadProviders(); + return updated; } else { const created = await createProvider(data as ProviderCreateRequest); await loadProviders();