feat: add post-setup health check page
This commit is contained in:
@@ -57,6 +57,17 @@ export interface InstallResponse {
|
|||||||
restart: boolean
|
restart: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface HealthComponentStatus {
|
||||||
|
status: string
|
||||||
|
healthy: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RuntimeHealth {
|
||||||
|
status: string
|
||||||
|
timestamp: string
|
||||||
|
components: Record<string, HealthComponentStatus>
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get setup status
|
* Get setup status
|
||||||
*/
|
*/
|
||||||
@@ -86,3 +97,18 @@ export async function install(config: InstallRequest): Promise<InstallResponse>
|
|||||||
const response = await setupClient.post('/setup/install', config)
|
const response = await setupClient.post('/setup/install', config)
|
||||||
return response.data.data
|
return response.data.data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get runtime health status after setup completes
|
||||||
|
*/
|
||||||
|
export async function getRuntimeHealth(): Promise<RuntimeHealth> {
|
||||||
|
const response = await setupClient.get('/health', {
|
||||||
|
validateStatus: () => true
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.status >= 500 && !response.data) {
|
||||||
|
throw new Error('Health check failed')
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|||||||
@@ -237,8 +237,29 @@ export default {
|
|||||||
completeInstallation: 'Complete Installation',
|
completeInstallation: 'Complete Installation',
|
||||||
completed: 'Installation completed!',
|
completed: 'Installation completed!',
|
||||||
redirecting: 'Redirecting to login page...',
|
redirecting: 'Redirecting to login page...',
|
||||||
|
redirectingHealth: 'Redirecting to the health check page...',
|
||||||
restarting: 'Service is restarting, please wait...',
|
restarting: 'Service is restarting, please wait...',
|
||||||
timeout: 'Service restart is taking longer than expected. Please refresh the page manually.'
|
timeout: 'Service restart is taking longer than expected. Please refresh the page manually.'
|
||||||
|
},
|
||||||
|
healthCheck: {
|
||||||
|
title: 'Post-Setup Health Check',
|
||||||
|
description: 'Confirm the service, database, and Redis are all ready after installation.',
|
||||||
|
checking: 'Checking runtime health...',
|
||||||
|
retryNow: 'Retry Check',
|
||||||
|
continueToLogin: 'Continue to Login',
|
||||||
|
overallHealthy: 'The system is ready. You can continue to login and verify the deployment.',
|
||||||
|
overallDegraded: 'The service is up, but some dependencies are not fully healthy yet.',
|
||||||
|
serviceUnavailable: 'The service is not reachable yet. Confirm the restart completed and try again.',
|
||||||
|
service: 'Service',
|
||||||
|
database: 'Database',
|
||||||
|
redis: 'Redis',
|
||||||
|
checkedAt: 'Checked at',
|
||||||
|
reportedAt: 'Backend reported at',
|
||||||
|
healthy: 'Healthy',
|
||||||
|
degraded: 'Degraded',
|
||||||
|
unavailable: 'Unavailable',
|
||||||
|
unknown: 'Unknown',
|
||||||
|
fromInstall: 'Initial setup has completed. Review this first-start health result before continuing.'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -237,8 +237,29 @@ export default {
|
|||||||
completeInstallation: '完成安装',
|
completeInstallation: '完成安装',
|
||||||
completed: '安装完成!',
|
completed: '安装完成!',
|
||||||
redirecting: '正在跳转到登录页面...',
|
redirecting: '正在跳转到登录页面...',
|
||||||
|
redirectingHealth: '正在跳转到健康检查页面...',
|
||||||
restarting: '服务正在重启,请稍候...',
|
restarting: '服务正在重启,请稍候...',
|
||||||
timeout: '服务重启时间超出预期,请手动刷新页面。'
|
timeout: '服务重启时间超出预期,请手动刷新页面。'
|
||||||
|
},
|
||||||
|
healthCheck: {
|
||||||
|
title: '初始化后健康检查',
|
||||||
|
description: '安装完成后,确认服务、数据库和 Redis 已全部就绪。',
|
||||||
|
checking: '正在检查服务状态...',
|
||||||
|
retryNow: '重新检查',
|
||||||
|
continueToLogin: '继续前往登录',
|
||||||
|
overallHealthy: '系统已准备就绪,可以继续登录和上线验证。',
|
||||||
|
overallDegraded: '系统已经启动,但仍有依赖未完全就绪,请先排查后再继续。',
|
||||||
|
serviceUnavailable: '当前无法连接到服务,请确认服务已完成重启并对外提供访问。',
|
||||||
|
service: '服务进程',
|
||||||
|
database: '数据库',
|
||||||
|
redis: 'Redis',
|
||||||
|
checkedAt: '本地检查时间',
|
||||||
|
reportedAt: '后端上报时间',
|
||||||
|
healthy: '健康',
|
||||||
|
degraded: '降级',
|
||||||
|
unavailable: '不可达',
|
||||||
|
unknown: '未知',
|
||||||
|
fromInstall: '系统初始化已完成,下面是首次启动健康检查结果。'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -340,6 +340,17 @@ describe('路由守卫逻辑', () => {
|
|||||||
expect(redirect).toBeNull()
|
expect(redirect).toBeNull()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('unauthenticated: /setup/health is allowed', () => {
|
||||||
|
const authState: MockAuthState = {
|
||||||
|
isAuthenticated: false,
|
||||||
|
isAdmin: false,
|
||||||
|
isSimpleMode: false,
|
||||||
|
backendModeEnabled: true,
|
||||||
|
}
|
||||||
|
const redirect = simulateGuard('/setup/health', { requiresAuth: false }, authState)
|
||||||
|
expect(redirect).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
it('admin: /admin/dashboard is allowed', () => {
|
it('admin: /admin/dashboard is allowed', () => {
|
||||||
const authState: MockAuthState = {
|
const authState: MockAuthState = {
|
||||||
isAuthenticated: true,
|
isAuthenticated: true,
|
||||||
|
|||||||
@@ -25,6 +25,15 @@ const routes: RouteRecordRaw[] = [
|
|||||||
title: 'Setup'
|
title: 'Setup'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/setup/health',
|
||||||
|
name: 'SetupHealthCheck',
|
||||||
|
component: () => import('@/views/setup/SetupHealthCheckView.vue'),
|
||||||
|
meta: {
|
||||||
|
requiresAuth: false,
|
||||||
|
title: 'Setup Health Check'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// ==================== Public Routes ====================
|
// ==================== Public Routes ====================
|
||||||
{
|
{
|
||||||
|
|||||||
325
frontend/src/views/setup/SetupHealthCheckView.vue
Normal file
325
frontend/src/views/setup/SetupHealthCheckView.vue
Normal file
@@ -0,0 +1,325 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex min-h-screen items-center justify-center bg-gradient-to-br from-gray-50 to-gray-100 p-4 dark:from-dark-900 dark:to-dark-800">
|
||||||
|
<div class="w-full max-w-4xl">
|
||||||
|
<div class="mb-8 text-center">
|
||||||
|
<div class="mb-4 inline-flex h-16 w-16 items-center justify-center rounded-2xl bg-gradient-to-br from-emerald-500 to-teal-600 shadow-lg">
|
||||||
|
<Icon name="checkCircle" size="xl" class="text-white" />
|
||||||
|
</div>
|
||||||
|
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">{{ t('setup.healthCheck.title') }}</h1>
|
||||||
|
<p class="mt-2 text-gray-500 dark:text-dark-400">{{ t('setup.healthCheck.description') }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-2xl bg-white p-8 shadow-xl dark:bg-dark-800">
|
||||||
|
<div
|
||||||
|
v-if="fromInstall"
|
||||||
|
class="mb-6 rounded-xl border border-blue-200 bg-blue-50 px-4 py-3 text-sm text-blue-700 dark:border-blue-900/40 dark:bg-blue-900/20 dark:text-blue-300"
|
||||||
|
>
|
||||||
|
{{ t('setup.healthCheck.fromInstall') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="mb-6 rounded-xl border px-4 py-4"
|
||||||
|
:class="overallStatusClass"
|
||||||
|
>
|
||||||
|
<div class="flex items-start gap-3">
|
||||||
|
<svg
|
||||||
|
v-if="checking"
|
||||||
|
class="mt-0.5 h-5 w-5 flex-shrink-0 animate-spin"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||||
|
<path
|
||||||
|
class="opacity-75"
|
||||||
|
fill="currentColor"
|
||||||
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<Icon v-else :name="overallIcon" size="md" class="mt-0.5 flex-shrink-0" />
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-semibold">{{ overallTitle }}</p>
|
||||||
|
<p class="mt-1 text-sm opacity-90">{{ overallDescription }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid gap-4 md:grid-cols-3">
|
||||||
|
<div
|
||||||
|
v-for="item in healthCards"
|
||||||
|
:key="item.key"
|
||||||
|
class="rounded-2xl border p-5"
|
||||||
|
:class="item.cardClass"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="text-sm font-medium">{{ item.label }}</span>
|
||||||
|
<Icon :name="item.icon" size="md" />
|
||||||
|
</div>
|
||||||
|
<p class="mt-4 text-2xl font-semibold">{{ item.statusText }}</p>
|
||||||
|
<p class="mt-1 text-sm opacity-80">{{ item.detail }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-6 grid gap-3 rounded-2xl bg-gray-50 p-4 text-sm text-gray-600 dark:bg-dark-700 dark:text-dark-300 md:grid-cols-2">
|
||||||
|
<div>
|
||||||
|
<span class="font-medium text-gray-900 dark:text-white">{{ t('setup.healthCheck.checkedAt') }}:</span>
|
||||||
|
{{ checkedAtLabel }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="font-medium text-gray-900 dark:text-white">{{ t('setup.healthCheck.reportedAt') }}:</span>
|
||||||
|
{{ reportedAtLabel }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p v-if="errorMessage" class="mt-4 text-sm text-red-600 dark:text-red-400">{{ errorMessage }}</p>
|
||||||
|
|
||||||
|
<div class="mt-8 flex flex-col gap-3 sm:flex-row sm:justify-between">
|
||||||
|
<button
|
||||||
|
@click="runChecks"
|
||||||
|
:disabled="checking"
|
||||||
|
class="btn btn-secondary"
|
||||||
|
>
|
||||||
|
<Icon name="refresh" size="sm" class="mr-2" :stroke-width="2" />
|
||||||
|
{{ t('setup.healthCheck.retryNow') }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="goToLogin"
|
||||||
|
:disabled="!serviceReachable"
|
||||||
|
class="btn btn-primary"
|
||||||
|
>
|
||||||
|
{{ t('setup.healthCheck.continueToLogin') }}
|
||||||
|
<Icon name="chevronRight" size="sm" class="ml-2" :stroke-width="2" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import Icon from '@/components/icons/Icon.vue'
|
||||||
|
import { getRuntimeHealth, getSetupStatus, type RuntimeHealth } from '@/api/setup'
|
||||||
|
|
||||||
|
type CardState = 'healthy' | 'degraded' | 'unavailable' | 'unknown'
|
||||||
|
type HealthIconName = 'checkCircle' | 'exclamationTriangle' | 'xCircle' | 'infoCircle'
|
||||||
|
|
||||||
|
interface HealthCard {
|
||||||
|
key: string
|
||||||
|
label: string
|
||||||
|
state: CardState
|
||||||
|
detail: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const { t, locale } = useI18n()
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
const checking = ref(true)
|
||||||
|
const errorMessage = ref('')
|
||||||
|
const serviceReachable = ref(false)
|
||||||
|
const runtimeHealth = ref<RuntimeHealth | null>(null)
|
||||||
|
const checkedAt = ref<Date | null>(null)
|
||||||
|
|
||||||
|
const pollIntervalMs = 3000
|
||||||
|
const maxPollAttempts = 20
|
||||||
|
const pollAttempts = ref(0)
|
||||||
|
let pollTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
|
const fromInstall = computed(() => route.query.from === 'install')
|
||||||
|
|
||||||
|
const componentState = (key: string): CardState => {
|
||||||
|
const component = runtimeHealth.value?.components?.[key]
|
||||||
|
if (!component) {
|
||||||
|
return serviceReachable.value ? 'unknown' : 'unavailable'
|
||||||
|
}
|
||||||
|
if (component.healthy) {
|
||||||
|
return 'healthy'
|
||||||
|
}
|
||||||
|
return component.status === 'unhealthy' ? 'degraded' : 'unknown'
|
||||||
|
}
|
||||||
|
|
||||||
|
const overallState = computed<CardState>(() => {
|
||||||
|
if (!serviceReachable.value) {
|
||||||
|
return 'unavailable'
|
||||||
|
}
|
||||||
|
if (runtimeHealth.value?.status === 'ok') {
|
||||||
|
return 'healthy'
|
||||||
|
}
|
||||||
|
if (runtimeHealth.value?.status === 'degraded') {
|
||||||
|
return 'degraded'
|
||||||
|
}
|
||||||
|
return 'unknown'
|
||||||
|
})
|
||||||
|
|
||||||
|
const overallStatusClass = computed(() => {
|
||||||
|
switch (overallState.value) {
|
||||||
|
case 'healthy':
|
||||||
|
return 'border-emerald-200 bg-emerald-50 text-emerald-700 dark:border-emerald-900/40 dark:bg-emerald-900/20 dark:text-emerald-300'
|
||||||
|
case 'degraded':
|
||||||
|
return 'border-amber-200 bg-amber-50 text-amber-700 dark:border-amber-900/40 dark:bg-amber-900/20 dark:text-amber-300'
|
||||||
|
case 'unavailable':
|
||||||
|
return 'border-red-200 bg-red-50 text-red-700 dark:border-red-900/40 dark:bg-red-900/20 dark:text-red-300'
|
||||||
|
default:
|
||||||
|
return 'border-gray-200 bg-gray-50 text-gray-700 dark:border-dark-700 dark:bg-dark-700 dark:text-dark-200'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const overallIcon = computed<HealthIconName>(() => {
|
||||||
|
switch (overallState.value) {
|
||||||
|
case 'healthy':
|
||||||
|
return 'checkCircle'
|
||||||
|
case 'degraded':
|
||||||
|
return 'exclamationTriangle'
|
||||||
|
case 'unavailable':
|
||||||
|
return 'xCircle'
|
||||||
|
default:
|
||||||
|
return 'infoCircle'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const overallTitle = computed(() => {
|
||||||
|
if (checking.value) {
|
||||||
|
return t('setup.healthCheck.checking')
|
||||||
|
}
|
||||||
|
switch (overallState.value) {
|
||||||
|
case 'healthy':
|
||||||
|
return t('setup.healthCheck.healthy')
|
||||||
|
case 'degraded':
|
||||||
|
return t('setup.healthCheck.degraded')
|
||||||
|
case 'unavailable':
|
||||||
|
return t('setup.healthCheck.unavailable')
|
||||||
|
default:
|
||||||
|
return t('setup.healthCheck.unknown')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const overallDescription = computed(() => {
|
||||||
|
switch (overallState.value) {
|
||||||
|
case 'healthy':
|
||||||
|
return t('setup.healthCheck.overallHealthy')
|
||||||
|
case 'degraded':
|
||||||
|
return t('setup.healthCheck.overallDegraded')
|
||||||
|
case 'unavailable':
|
||||||
|
return t('setup.healthCheck.serviceUnavailable')
|
||||||
|
default:
|
||||||
|
return t('setup.healthCheck.checking')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const checkedAtLabel = computed(() => {
|
||||||
|
return checkedAt.value ? checkedAt.value.toLocaleString(locale.value) : t('common.notAvailable')
|
||||||
|
})
|
||||||
|
|
||||||
|
const reportedAtLabel = computed(() => {
|
||||||
|
if (!runtimeHealth.value?.timestamp) {
|
||||||
|
return t('common.notAvailable')
|
||||||
|
}
|
||||||
|
const value = new Date(runtimeHealth.value.timestamp)
|
||||||
|
return Number.isNaN(value.getTime()) ? runtimeHealth.value.timestamp : value.toLocaleString(locale.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const cardMeta = (state: CardState): { icon: HealthIconName; statusText: string; cardClass: string } => {
|
||||||
|
switch (state) {
|
||||||
|
case 'healthy':
|
||||||
|
return {
|
||||||
|
icon: 'checkCircle',
|
||||||
|
statusText: t('setup.healthCheck.healthy'),
|
||||||
|
cardClass: 'border-emerald-200 bg-emerald-50 text-emerald-700 dark:border-emerald-900/40 dark:bg-emerald-900/20 dark:text-emerald-300'
|
||||||
|
}
|
||||||
|
case 'degraded':
|
||||||
|
return {
|
||||||
|
icon: 'exclamationTriangle',
|
||||||
|
statusText: t('setup.healthCheck.degraded'),
|
||||||
|
cardClass: 'border-amber-200 bg-amber-50 text-amber-700 dark:border-amber-900/40 dark:bg-amber-900/20 dark:text-amber-300'
|
||||||
|
}
|
||||||
|
case 'unavailable':
|
||||||
|
return {
|
||||||
|
icon: 'xCircle',
|
||||||
|
statusText: t('setup.healthCheck.unavailable'),
|
||||||
|
cardClass: 'border-red-200 bg-red-50 text-red-700 dark:border-red-900/40 dark:bg-red-900/20 dark:text-red-300'
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
icon: 'infoCircle',
|
||||||
|
statusText: t('setup.healthCheck.unknown'),
|
||||||
|
cardClass: 'border-gray-200 bg-gray-50 text-gray-700 dark:border-dark-700 dark:bg-dark-700 dark:text-dark-200'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const healthCards = computed(() => {
|
||||||
|
const cards: HealthCard[] = [
|
||||||
|
{ key: 'service', label: t('setup.healthCheck.service'), state: serviceReachable.value ? 'healthy' : 'unavailable', detail: '/health' },
|
||||||
|
{ key: 'database', label: t('setup.healthCheck.database'), state: componentState('database'), detail: runtimeHealth.value?.components?.database?.status ?? t('setup.healthCheck.unknown') },
|
||||||
|
{ key: 'redis', label: t('setup.healthCheck.redis'), state: componentState('redis'), detail: runtimeHealth.value?.components?.redis?.status ?? t('setup.healthCheck.unknown') }
|
||||||
|
]
|
||||||
|
|
||||||
|
return cards.map((card) => ({
|
||||||
|
...card,
|
||||||
|
...cardMeta(card.state)
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
function clearPollTimer() {
|
||||||
|
if (pollTimer !== null) {
|
||||||
|
clearTimeout(pollTimer)
|
||||||
|
pollTimer = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleRetry() {
|
||||||
|
if (overallState.value === 'healthy' || pollAttempts.value >= maxPollAttempts) {
|
||||||
|
clearPollTimer()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
clearPollTimer()
|
||||||
|
pollTimer = setTimeout(() => {
|
||||||
|
void runChecks()
|
||||||
|
}, pollIntervalMs)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runChecks() {
|
||||||
|
checking.value = true
|
||||||
|
errorMessage.value = ''
|
||||||
|
clearPollTimer()
|
||||||
|
|
||||||
|
try {
|
||||||
|
const setupStatus = await getSetupStatus()
|
||||||
|
if (setupStatus.needs_setup) {
|
||||||
|
await router.replace('/setup')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const health = await getRuntimeHealth()
|
||||||
|
runtimeHealth.value = health
|
||||||
|
serviceReachable.value = true
|
||||||
|
checkedAt.value = new Date()
|
||||||
|
pollAttempts.value += 1
|
||||||
|
} catch (error: unknown) {
|
||||||
|
serviceReachable.value = false
|
||||||
|
checkedAt.value = new Date()
|
||||||
|
pollAttempts.value += 1
|
||||||
|
const err = error as { message?: string }
|
||||||
|
errorMessage.value = err.message || t('common.unknownError')
|
||||||
|
} finally {
|
||||||
|
checking.value = false
|
||||||
|
scheduleRetry()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function goToLogin() {
|
||||||
|
window.location.href = '/login'
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
void runChecks()
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
clearPollTimer()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -425,7 +425,7 @@
|
|||||||
<p class="mt-1 text-sm text-green-600 dark:text-green-500">
|
<p class="mt-1 text-sm text-green-600 dark:text-green-500">
|
||||||
{{
|
{{
|
||||||
serviceReady
|
serviceReady
|
||||||
? t('setup.status.redirecting')
|
? t('setup.status.redirectingHealth')
|
||||||
: t('setup.status.restarting')
|
: t('setup.status.restarting')
|
||||||
}}
|
}}
|
||||||
</p>
|
</p>
|
||||||
@@ -654,9 +654,9 @@ async function waitForServiceRestart() {
|
|||||||
// If needs_setup is false, service has restarted in normal mode
|
// If needs_setup is false, service has restarted in normal mode
|
||||||
if (data.data && !data.data.needs_setup) {
|
if (data.data && !data.data.needs_setup) {
|
||||||
serviceReady.value = true
|
serviceReady.value = true
|
||||||
// Redirect to login page after a short delay
|
// Redirect to the post-setup health check page after a short delay
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.location.href = '/login'
|
window.location.href = '/setup/health?from=install'
|
||||||
}, 1500)
|
}, 1500)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user