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(