feat(fullstack): Add server health check with overlay
This commit is contained in:
@@ -5,6 +5,8 @@
|
||||
<NuxtLayout>
|
||||
<NuxtPage />
|
||||
</NuxtLayout>
|
||||
|
||||
<ServerConnectionOverlay />
|
||||
</UApp>
|
||||
</template>
|
||||
|
||||
|
||||
55
legalconsenthub/components/ServerConnectionOverlay.vue
Normal file
55
legalconsenthub/components/ServerConnectionOverlay.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="!isServerAvailable"
|
||||
class="fixed inset-0 z-50 bg-black/50 backdrop-blur-sm flex items-center justify-center"
|
||||
@click.prevent
|
||||
@keydown.prevent
|
||||
>
|
||||
<div class="bg-white dark:bg-gray-900 rounded-lg shadow-xl p-8 max-w-md mx-4 text-center">
|
||||
<!-- Loading Spinner -->
|
||||
<div class="mb-6 flex justify-center">
|
||||
<UIcon name="i-heroicons-arrow-path" class="w-12 h-12 text-primary-500 animate-spin" />
|
||||
</div>
|
||||
|
||||
<!-- Title -->
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">
|
||||
{{ t('serverConnection.title') }}
|
||||
</h2>
|
||||
|
||||
<!-- Description -->
|
||||
<p class="text-gray-600 dark:text-gray-400 mb-6 leading-relaxed">
|
||||
{{ t('serverConnection.message') }}
|
||||
</p>
|
||||
|
||||
<!-- Status Information -->
|
||||
<div class="space-y-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
<div v-if="isChecking" class="flex items-center justify-center gap-2">
|
||||
<UIcon name="i-heroicons-arrow-path" class="w-4 h-4 animate-spin" />
|
||||
<span>{{ t('serverConnection.checking') }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="lastCheckTime" class="text-xs">
|
||||
{{ t('serverConnection.lastCheck') }}:
|
||||
{{ new Date(lastCheckTime).toLocaleTimeString() }}
|
||||
</div>
|
||||
|
||||
<div class="text-xs">
|
||||
{{ t('serverConnection.retryInfo') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Optional: Manual retry button -->
|
||||
<UButton v-if="!isChecking" variant="ghost" size="sm" class="mt-4" @click="void checkServerHealth()">
|
||||
<UIcon name="i-heroicons-arrow-path" class="w-4 h-4 mr-1" />
|
||||
{{ t('serverConnection.retryNow') }}
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { t } = useI18n()
|
||||
const { isServerAvailable, isChecking, lastCheckTime, checkServerHealth } = useServerHealth()
|
||||
</script>
|
||||
92
legalconsenthub/composables/useServerHealth.ts
Normal file
92
legalconsenthub/composables/useServerHealth.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
export const isServerAvailable = ref(true)
|
||||
export const isChecking = ref(false)
|
||||
export const lastCheckTime = ref<Date | null>(null)
|
||||
|
||||
export function useServerHealth() {
|
||||
const checkInterval = ref<NodeJS.Timeout | null>(null)
|
||||
const healthCheckUrl = 'http://localhost:3001/api/actuator/health'
|
||||
|
||||
async function checkServerHealth(): Promise<boolean> {
|
||||
if (isChecking.value) return isServerAvailable.value
|
||||
|
||||
isChecking.value = true
|
||||
lastCheckTime.value = new Date()
|
||||
|
||||
try {
|
||||
const controller = new AbortController()
|
||||
const timeoutId = setTimeout(() => controller.abort(), 5000)
|
||||
|
||||
const response = await fetch(healthCheckUrl, {
|
||||
method: 'GET',
|
||||
signal: controller.signal,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
clearTimeout(timeoutId)
|
||||
|
||||
const wasAvailable = isServerAvailable.value
|
||||
isServerAvailable.value = response.ok
|
||||
|
||||
if (!wasAvailable && isServerAvailable.value) {
|
||||
console.info('Server is back online')
|
||||
}
|
||||
|
||||
if (wasAvailable && !isServerAvailable.value) {
|
||||
console.warn('Server is no longer available')
|
||||
}
|
||||
|
||||
return isServerAvailable.value
|
||||
} catch (error) {
|
||||
const wasAvailable = isServerAvailable.value
|
||||
isServerAvailable.value = false
|
||||
|
||||
if (wasAvailable) {
|
||||
console.warn('Server health check failed:', error)
|
||||
}
|
||||
|
||||
return false
|
||||
} finally {
|
||||
isChecking.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function startPeriodicHealthCheck(intervalMs: number = 60000) {
|
||||
if (checkInterval.value) {
|
||||
clearInterval(checkInterval.value)
|
||||
}
|
||||
|
||||
checkServerHealth()
|
||||
|
||||
checkInterval.value = setInterval(() => {
|
||||
checkServerHealth()
|
||||
}, intervalMs)
|
||||
|
||||
onUnmounted(() => {
|
||||
if (checkInterval.value) {
|
||||
clearInterval(checkInterval.value)
|
||||
checkInterval.value = null
|
||||
}
|
||||
})
|
||||
|
||||
return checkInterval.value
|
||||
}
|
||||
|
||||
const stopHealthCheck = () => {
|
||||
if (checkInterval.value) {
|
||||
clearInterval(checkInterval.value)
|
||||
checkInterval.value = null
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isServerAvailable,
|
||||
isChecking,
|
||||
lastCheckTime,
|
||||
healthCheckUrl,
|
||||
checkServerHealth,
|
||||
startPeriodicHealthCheck,
|
||||
stopHealthCheck
|
||||
}
|
||||
}
|
||||
@@ -5,5 +5,13 @@
|
||||
"employer": "Arbeitgeber",
|
||||
"worksCouncilMember": "Betriebsratsmitglied",
|
||||
"worksCouncilChair": "Betriebsratsvorsitzender"
|
||||
},
|
||||
"serverConnection": {
|
||||
"title": "Verbindung zum Server unterbrochen",
|
||||
"message": "Die Verbindung zum Server ist momentan nicht verfügbar. Wir versuchen automatisch, die Verbindung wiederherzustellen.",
|
||||
"checking": "Verbindung wird geprüft...",
|
||||
"lastCheck": "Letzte Überprüfung",
|
||||
"retryInfo": "Automatischer Wiederholungsversuch alle 60 Sekunden",
|
||||
"retryNow": "Jetzt erneut versuchen"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,5 +5,13 @@
|
||||
"employer": "Employer",
|
||||
"worksCouncilMember": "Works Council Member",
|
||||
"worksCouncilChair": "Works Council Chair"
|
||||
},
|
||||
"serverConnection": {
|
||||
"title": "Server Connection Lost",
|
||||
"message": "The connection to the server is currently unavailable. We are automatically trying to restore the connection.",
|
||||
"checking": "Checking connection...",
|
||||
"lastCheck": "Last check",
|
||||
"retryInfo": "Automatic retry every 60 seconds",
|
||||
"retryNow": "Try again now"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"fix:bettersqlite": "cd node_modules/better-sqlite3 && pnpm dlx node-gyp rebuild && cd ../..",
|
||||
"generate:betterauth": "pnpm dlx @better-auth/cli generate --config server/utils/auth.ts --yes",
|
||||
"migrate:betterauth": "pnpm dlx @better-auth/cli migrate --config server/utils/auth.ts --yes",
|
||||
"recreate-db:betterauth": "rm sqlite.db && pnpm run migrate:betterauth && pnpm run migrate:betterauth"
|
||||
"recreate-db:betterauth": "[ -f sqlite.db ] && rm sqlite.db; pnpm run migrate:betterauth"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nuxt/ui-pro": "3.1.1",
|
||||
|
||||
@@ -118,7 +118,6 @@ function onSubmit(payload: FormSubmitEvent<Schema>) {
|
||||
},
|
||||
onError: async (ctx) => {
|
||||
console.log(ctx.error.message)
|
||||
await deleteUser({ callbackURL: '/signup' })
|
||||
useToast().add({
|
||||
title: 'Fehler bei der Registrierung',
|
||||
description: ctx.error.message,
|
||||
|
||||
13
legalconsenthub/plugins/server-health.client.ts
Normal file
13
legalconsenthub/plugins/server-health.client.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export default defineNuxtPlugin(() => {
|
||||
// This plugin runs only on the client side to avoid issues with server-side rendering
|
||||
if (import.meta.client) {
|
||||
// Initialize server health monitoring as soon as the client is ready
|
||||
const { startPeriodicHealthCheck } = useServerHealth()
|
||||
|
||||
// Start the health check with a 1-minute interval
|
||||
// This ensures the health check starts even if app.vue's onMounted hasn't fired yet
|
||||
nextTick(() => {
|
||||
startPeriodicHealthCheck(60000)
|
||||
})
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user