feat(translation): "Test connection" button — on-demand Interpreter health check (#143)
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) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CDgx8bQS5YrGRK76v8HUnM
This commit is contained in:
@@ -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"])
|
@settings_bp.route("/settings/translation/run", methods=["POST"])
|
||||||
async def translation_run():
|
async def translation_run():
|
||||||
"""Enqueue the translate sweep now (the Settings 'Translate now' button)."""
|
"""Enqueue the translate sweep now (the Settings 'Translate now' button)."""
|
||||||
|
|||||||
@@ -28,9 +28,17 @@
|
|||||||
@change="onSave"
|
@change="onSave"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="d-flex align-center mb-3" style="gap: 8px;">
|
<div class="d-flex align-center flex-wrap mb-3" style="gap: 8px;">
|
||||||
<v-icon size="12" :color="statusColor">mdi-circle</v-icon>
|
<v-icon size="12" :color="statusColor">mdi-circle</v-icon>
|
||||||
<span class="fc-muted text-body-2">{{ statusText }}</span>
|
<span class="fc-muted text-body-2">{{ statusText }}</span>
|
||||||
|
<v-btn
|
||||||
|
size="x-small" variant="tonal" rounded="pill" class="ml-2"
|
||||||
|
:loading="testing" :disabled="!baseUrl" @click="onTest"
|
||||||
|
>Test connection</v-btn>
|
||||||
|
<span
|
||||||
|
v-if="testResult !== null" class="text-caption"
|
||||||
|
:class="testResult ? 'text-success' : 'text-error'"
|
||||||
|
>{{ testResult ? '✓ reachable' : '✗ unreachable' }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-flex align-center flex-wrap" style="gap: 10px;">
|
<div class="d-flex align-center flex-wrap" style="gap: 10px;">
|
||||||
@@ -67,6 +75,8 @@ const baseUrl = ref('')
|
|||||||
const targetLang = ref('en')
|
const targetLang = ref('en')
|
||||||
const busy = ref(false)
|
const busy = ref(false)
|
||||||
const running = 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 status = ref(null)
|
||||||
const err = ref(null)
|
const err = ref(null)
|
||||||
|
|
||||||
@@ -86,6 +96,25 @@ async function loadStatus() {
|
|||||||
catch { status.value = null }
|
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 () => {
|
onMounted(async () => {
|
||||||
try {
|
try {
|
||||||
await store.loadSettings()
|
await store.loadSettings()
|
||||||
|
|||||||
@@ -69,6 +69,20 @@ async def test_translation_run_requires_config(client):
|
|||||||
assert resp.status_code == 400 # disabled / no base URL
|
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
|
@pytest.mark.asyncio
|
||||||
async def test_translation_run_enqueues_when_configured(client, monkeypatch):
|
async def test_translation_run_enqueues_when_configured(client, monkeypatch):
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
|
|||||||
Reference in New Issue
Block a user