6a255482ea
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
163 lines
5.3 KiB
Vue
163 lines
5.3 KiB
Vue
<template>
|
|
<MaintenanceTile
|
|
icon="mdi-translate"
|
|
title="Post translation (Interpreter)"
|
|
blurb="Translate non-English post titles + descriptions to English via your self-hosted Interpreter service, so archived posts read naturally."
|
|
>
|
|
<div class="d-flex align-center mb-3" style="gap: 10px;">
|
|
<span class="fc-section-h">Enabled</span>
|
|
<v-switch
|
|
v-model="enabled" :loading="busy" hide-details density="compact"
|
|
color="success" class="ml-auto" @update:model-value="onToggle"
|
|
/>
|
|
</div>
|
|
<p class="fc-muted text-body-2 mb-3">
|
|
Point this at your Interpreter service. It stays off until a URL is set, and
|
|
only translates posts whose text isn't already in the target language — the
|
|
English is shown by default on post cards, with a toggle back to the original.
|
|
</p>
|
|
|
|
<v-text-field
|
|
v-model="baseUrl" label="Interpreter base URL"
|
|
placeholder="http://interpreter.your-domain/" density="compact"
|
|
hide-details class="mb-3" :disabled="busy" @change="onSave"
|
|
/>
|
|
<v-text-field
|
|
v-model="targetLang" label="Target language" density="compact"
|
|
hide-details style="max-width: 160px;" class="mb-3" :disabled="busy"
|
|
@change="onSave"
|
|
/>
|
|
|
|
<div class="d-flex align-center flex-wrap mb-3" style="gap: 8px;">
|
|
<v-icon size="12" :color="statusColor">mdi-circle</v-icon>
|
|
<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 class="d-flex align-center flex-wrap" style="gap: 10px;">
|
|
<v-btn
|
|
size="small" variant="flat" color="accent" rounded="pill"
|
|
prepend-icon="mdi-translate" :loading="running"
|
|
:disabled="!enabled || !baseUrl" @click="onRun"
|
|
>Translate now</v-btn>
|
|
<span v-if="status" class="fc-muted text-caption">
|
|
{{ status.untranslated_count }}
|
|
post{{ status.untranslated_count === 1 ? '' : 's' }} awaiting translation
|
|
</span>
|
|
</div>
|
|
|
|
<v-alert
|
|
v-if="err" type="error" variant="tonal" density="compact"
|
|
class="mt-3" closable @click:close="err = null"
|
|
>{{ err }}</v-alert>
|
|
</MaintenanceTile>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed, onMounted, ref } from 'vue'
|
|
import { toast } from '../../utils/toast.js'
|
|
import { useApi } from '../../composables/useApi.js'
|
|
import { useImportStore } from '../../stores/import.js'
|
|
import MaintenanceTile from '../common/MaintenanceTile.vue'
|
|
|
|
const api = useApi()
|
|
const store = useImportStore()
|
|
|
|
const enabled = ref(false)
|
|
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)
|
|
|
|
const statusColor = computed(() => {
|
|
if (!enabled.value || !baseUrl.value) return 'grey'
|
|
return status.value?.healthy ? 'success' : 'error'
|
|
})
|
|
const statusText = computed(() => {
|
|
if (!enabled.value) return 'Off'
|
|
if (!baseUrl.value) return 'No base URL set'
|
|
if (status.value == null) return 'Checking…'
|
|
return status.value.healthy ? 'Interpreter reachable' : 'Interpreter unreachable'
|
|
})
|
|
|
|
async function loadStatus() {
|
|
try { status.value = await api.get('/api/settings/translation/status') }
|
|
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()
|
|
const s = store.settings || {}
|
|
enabled.value = !!s.translation_enabled
|
|
baseUrl.value = s.interpreter_base_url || ''
|
|
targetLang.value = s.translation_target_lang || 'en'
|
|
} catch { /* non-fatal */ }
|
|
await loadStatus()
|
|
})
|
|
|
|
async function save(patch) {
|
|
busy.value = true
|
|
err.value = null
|
|
try { await store.patchSettings(patch) }
|
|
catch (e) { err.value = e.message }
|
|
finally { busy.value = false }
|
|
}
|
|
|
|
async function onToggle(val) {
|
|
await save({ translation_enabled: !!val })
|
|
toast({ text: val ? 'Translation on' : 'Translation off', type: 'success' })
|
|
await loadStatus()
|
|
}
|
|
async function onSave() {
|
|
await save({
|
|
interpreter_base_url: baseUrl.value.trim(),
|
|
translation_target_lang: (targetLang.value || 'en').trim() || 'en',
|
|
})
|
|
await loadStatus()
|
|
}
|
|
async function onRun() {
|
|
running.value = true
|
|
err.value = null
|
|
try {
|
|
await api.post('/api/settings/translation/run')
|
|
toast({ text: 'Translation sweep started', type: 'success' })
|
|
} catch (e) {
|
|
err.value = e.message
|
|
} finally {
|
|
running.value = false
|
|
setTimeout(loadStatus, 1500) // let the sweep make a dent, then refresh
|
|
}
|
|
}
|
|
</script>
|