feat: warn when classroom speech server is unavailable

This commit is contained in:
itsrubberduck
2026-04-23 09:21:23 +02:00
parent 3038f0f611
commit b7df1e86f2
2 changed files with 142 additions and 0 deletions

View File

@@ -1322,6 +1322,39 @@
</div>
</v-dialog>
<v-dialog v-model="showSpeechServerWarning" max-width="620" persistent>
<div class="panel dialog speech-server-dialog">
<div class="speech-server-icon">
<v-icon size="34">mdi-alert-circle-outline</v-icon>
</div>
<div>
<p class="eyebrow">Sprachserver aktuell nicht erreichbar</p>
<h3 class="h3">Server-TTS ist gerade überlastet</h3>
<p class="muted speech-server-copy">
Unser Sprachserver ist aktuell wegen zu hoher Auslastung nicht erreichbar. Es kann ein bisschen dauern,
bis dieses Feature wieder verfügbar ist. Du kannst solange Browser TTS nutzen.
</p>
</div>
<div class="speech-server-actions">
<button
class="btn primary"
type="button"
:disabled="!browserTtsAvailable"
@click="enableBrowserTtsFromWarning"
>
<v-icon size="18">mdi-volume-high</v-icon>
Browser TTS nutzen
</button>
<button class="btn ghost" type="button" @click="showSpeechServerWarning=false">
Verstanden
</button>
</div>
<p v-if="!browserTtsAvailable" class="muted small">
Dein Browser meldet aktuell keine Web-Speech-Unterstützung.
</p>
</div>
</v-dialog>
<!-- TOAST -->
<v-snackbar v-model="toast.show" timeout="2200" location="top" color="#22d3ee">
<v-icon class="mr-2">mdi-trophy</v-icon>
@@ -2710,9 +2743,16 @@ const referenceOpen = ref(false)
const toast = ref({show: false, text: ''})
const showSettings = ref(false)
const showSpeechServerWarning = ref(false)
const api = useApi()
const isClient = typeof window !== 'undefined'
const auth = useAuthStore()
const browserTtsAvailable = computed(() => isClient && 'speechSynthesis' in window)
type SpeechServerHealth = {
configured: boolean
reachable: boolean
}
const ATC_SETTINGS_STORAGE_PREFIX = 'os_atc_settings_'
@@ -2769,6 +2809,20 @@ function persistLocalAtcSettings(config: LearnConfig) {
}
}
async function checkSpeechServerAvailability() {
try {
const health = await api.get<SpeechServerHealth>('/api/classroom/speech-server-health')
showSpeechServerWarning.value = Boolean(health.configured && !health.reachable)
} catch (error) {
console.error('Failed to check speech server availability', error)
}
}
function enableBrowserTtsFromWarning() {
cfg.value.tts = true
showSpeechServerWarning.value = false
}
function loadLocalAtcSettings(): LearnConfigPatch | null {
if (!isClient) return null
const key = getAtcSettingsStorageKey()
@@ -4576,6 +4630,7 @@ onMounted(() => {
simbriefForm.userId = storedId
}
}
void checkSpeechServerAvailability()
})
</script>
@@ -6707,6 +6762,40 @@ onMounted(() => {
max-width: 70%;
}
.speech-server-dialog {
display: grid;
gap: 16px;
position: relative;
}
.speech-server-icon {
width: 58px;
height: 58px;
border-radius: 18px;
display: grid;
place-items: center;
color: #fbbf24;
background: color-mix(in srgb, #f59e0b 18%, transparent);
border: 1px solid color-mix(in srgb, #f59e0b 36%, transparent);
box-shadow: 0 18px 36px rgba(245, 158, 11, .16);
}
.speech-server-copy {
margin-top: 10px;
line-height: 1.6;
}
.speech-server-actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
justify-content: flex-end;
}
.speech-server-actions .btn {
justify-content: center;
}
.sr-only {
position: absolute;
width: 1px;

View File

@@ -0,0 +1,53 @@
import { defineEventHandler } from 'h3'
import { requireUserSession } from '../../utils/auth'
import { getServerRuntimeConfig } from '../../utils/runtimeConfig'
type SpeechServerHealth = {
configured: boolean
reachable: boolean
}
function buildHealthUrls(baseUrl: string): string[] {
const trimmed = baseUrl.replace(/\/+$/, '')
return [
`${trimmed}/health`,
`${trimmed}/v1/models`,
trimmed,
]
}
async function canReach(url: string): Promise<boolean> {
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), 2500)
try {
const response = await fetch(url, {
method: 'GET',
signal: controller.signal,
})
return response.status < 500
} catch {
return false
} finally {
clearTimeout(timeout)
}
}
export default defineEventHandler(async (event): Promise<SpeechServerHealth> => {
await requireUserSession(event)
const { speachesBaseUrl } = getServerRuntimeConfig()
if (!speachesBaseUrl) {
return {
configured: false,
reachable: false,
}
}
const results = await Promise.all(buildHealthUrls(speachesBaseUrl).map(url => canReach(url)))
return {
configured: true,
reachable: results.some(Boolean),
}
})