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(