From 10fc40e5be78611b3886e1d2fd932587e48f43b3 Mon Sep 17 00:00:00 2001 From: itsrubberduck Date: Thu, 23 Apr 2026 09:21:23 +0200 Subject: [PATCH] feat: warn when classroom speech server is unavailable --- app/pages/classroom.vue | 89 +++++++++++++++++++ .../api/classroom/speech-server-health.get.ts | 53 +++++++++++ 2 files changed, 142 insertions(+) create mode 100644 server/api/classroom/speech-server-health.get.ts diff --git a/app/pages/classroom.vue b/app/pages/classroom.vue index 8598e7e..8e5800d 100644 --- a/app/pages/classroom.vue +++ b/app/pages/classroom.vue @@ -1322,6 +1322,39 @@ + +
+
+ mdi-alert-circle-outline +
+
+

Sprachserver aktuell nicht erreichbar

+

Server-TTS ist gerade überlastet

+

+ 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. +

+
+
+ + +
+

+ Dein Browser meldet aktuell keine Web-Speech-Unterstützung. +

+
+
+ mdi-trophy @@ -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('/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() }) @@ -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; diff --git a/server/api/classroom/speech-server-health.get.ts b/server/api/classroom/speech-server-health.get.ts new file mode 100644 index 0000000..69982b3 --- /dev/null +++ b/server/api/classroom/speech-server-health.get.ts @@ -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 { + 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 => { + 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), + } +})