From 6a255482ea0f0e81b4960960229bc9e39b54e308 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Tue, 7 Jul 2026 13:00:37 -0400 Subject: [PATCH] =?UTF-8?q?feat(translation):=20"Test=20connection"=20butt?= =?UTF-8?q?on=20=E2=80=94=20on-demand=20Interpreter=20health=20check=20(#1?= =?UTF-8?q?43)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New POST /api/settings/translation/test pings /v1/health for a GIVEN base URL (not the saved one), so the operator can verify a URL before enabling it. TranslationCard gains a Test-connection button that reports reachable/unreachable inline and updates the status dot. Endpoint test. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01CDgx8bQS5YrGRK76v8HUnM --- backend/app/api/settings.py | 13 ++++++++ .../components/settings/TranslationCard.vue | 31 ++++++++++++++++++- tests/test_api_settings.py | 14 +++++++++ 3 files changed, 57 insertions(+), 1 deletion(-) diff --git a/backend/app/api/settings.py b/backend/app/api/settings.py index f888430..12e850e 100644 --- a/backend/app/api/settings.py +++ b/backend/app/api/settings.py @@ -325,6 +325,19 @@ async def translation_status(): }) +@settings_bp.route("/settings/translation/test", methods=["POST"]) +async def translation_test(): + """On-demand reachability check for a GIVEN Interpreter base URL (the Settings + 'Test connection' button) — pings /v1/health without saving, so the operator + can verify a URL before enabling. Health runs in a thread (sync client).""" + body = await request.get_json() + base_url = "" + if isinstance(body, dict): + base_url = (body.get("base_url") or "").strip() + healthy = await asyncio.to_thread(ic.health, base_url) if base_url else False + return jsonify({"healthy": healthy}) + + @settings_bp.route("/settings/translation/run", methods=["POST"]) async def translation_run(): """Enqueue the translate sweep now (the Settings 'Translate now' button).""" diff --git a/frontend/src/components/settings/TranslationCard.vue b/frontend/src/components/settings/TranslationCard.vue index 5629267..728d4e6 100644 --- a/frontend/src/components/settings/TranslationCard.vue +++ b/frontend/src/components/settings/TranslationCard.vue @@ -28,9 +28,17 @@ @change="onSave" /> -
+
mdi-circle {{ statusText }} + Test connection + {{ testResult ? '✓ reachable' : '✗ unreachable' }}
@@ -67,6 +75,8 @@ const baseUrl = ref('') const targetLang = ref('en') const busy = ref(false) const running = ref(false) +const testing = ref(false) +const testResult = ref(null) // null = not tested this session; true/false = last result const status = ref(null) const err = ref(null) @@ -86,6 +96,25 @@ async function loadStatus() { catch { status.value = null } } +async function onTest() { + // Ping /v1/health for the CURRENTLY-typed URL (not the saved one), so a new + // URL can be verified before enabling. Also reflects in the status dot. + testing.value = true + err.value = null + testResult.value = null + try { + const r = await api.post('/api/settings/translation/test', { + body: { base_url: baseUrl.value.trim() }, + }) + testResult.value = !!r.healthy + if (status.value) status.value.healthy = !!r.healthy + } catch (e) { + err.value = e.message + } finally { + testing.value = false + } +} + onMounted(async () => { try { await store.loadSettings() diff --git a/tests/test_api_settings.py b/tests/test_api_settings.py index 73ed908..28b4fed 100644 --- a/tests/test_api_settings.py +++ b/tests/test_api_settings.py @@ -69,6 +69,20 @@ async def test_translation_run_requires_config(client): assert resp.status_code == 400 # disabled / no base URL +@pytest.mark.asyncio +async def test_translation_test_connection(client, monkeypatch): + # Tests a GIVEN url (not the saved one) without persisting anything. + monkeypatch.setattr("backend.app.api.settings.ic.health", lambda *a, **k: True) + resp = await client.post( + "/api/settings/translation/test", json={"base_url": "http://i.lan"}) + assert resp.status_code == 200 + assert (await resp.get_json())["healthy"] is True + # Empty URL → False, no health call. + empty = await client.post( + "/api/settings/translation/test", json={"base_url": ""}) + assert (await empty.get_json())["healthy"] is False + + @pytest.mark.asyncio async def test_translation_run_enqueues_when_configured(client, monkeypatch): monkeypatch.setattr(