2026-08-09 08:52:28 +00:00
|
|
|
|
import { useEffect, useState } from 'react'
|
|
|
|
|
|
import classNames from 'classnames'
|
2026-08-13 08:41:48 +00:00
|
|
|
|
import PageTitle from '@/components/shared/PageTitle'
|
2026-08-09 08:52:28 +00:00
|
|
|
|
import { Button } from '@/components/ui'
|
|
|
|
|
|
import { APP_NAME } from '@/constants/app.constant'
|
|
|
|
|
|
import { ROUTES_ENUM } from '@/routes/route.constant'
|
|
|
|
|
|
import { pollUntilServerReady } from '@/services/setup.service'
|
|
|
|
|
|
import { useStoreState } from '@/store'
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Uygulama konfigürasyonu alınamadığında gösterilir (tipik olarak veritabanı erişilemez
|
|
|
|
|
|
* durumdayken).
|
|
|
|
|
|
*
|
|
|
|
|
|
* Kullanıcı bilinçli olarak kurulum sihirbazına yönlendirilmez — çalışan bir sistemde
|
|
|
|
|
|
* geçici bir arıza kimseyi kurulum ekranına düşürmemeli. Bunun yerine durum açıkça
|
|
|
|
|
|
* belirtilir, sunucu arka planda izlenir ve toparlandığı anda sayfa kendini yeniler.
|
|
|
|
|
|
*/
|
|
|
|
|
|
const ServiceUnavailable = () => {
|
|
|
|
|
|
const isDark = useStoreState((state) => state.theme.mode) === 'dark'
|
|
|
|
|
|
const [attempt, setAttempt] = useState(0)
|
|
|
|
|
|
|
|
|
|
|
|
// Sunucu toparlandığında kullanıcının sayfayı elle yenilemesi gerekmesin.
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const cancel = pollUntilServerReady(
|
|
|
|
|
|
() => window.location.reload(),
|
|
|
|
|
|
(currentAttempt) => setAttempt(currentAttempt),
|
|
|
|
|
|
)
|
|
|
|
|
|
return cancel
|
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div
|
|
|
|
|
|
className={classNames(
|
|
|
|
|
|
'min-h-screen flex flex-col items-center justify-center p-6 transition-colors',
|
|
|
|
|
|
isDark ? 'bg-gray-900 text-gray-100' : 'bg-gray-100 text-gray-900',
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
2026-08-13 08:41:48 +00:00
|
|
|
|
<PageTitle title="Service unavailable" />
|
2026-08-09 08:52:28 +00:00
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
|
className={classNames(
|
|
|
|
|
|
'w-full max-w-lg overflow-hidden rounded-xl border shadow-2xl',
|
|
|
|
|
|
isDark ? 'border-gray-700 bg-gray-800' : 'border-gray-200 bg-white',
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
|
|
|
|
|
{/* Başlık */}
|
|
|
|
|
|
<div
|
|
|
|
|
|
className={classNames(
|
|
|
|
|
|
'flex items-center gap-3 border-b px-6 py-4',
|
|
|
|
|
|
isDark ? 'border-gray-700 bg-gray-700' : 'border-gray-200 bg-gray-50',
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<img
|
|
|
|
|
|
src={isDark ? '/img/logo/logo-dark-streamline.png' : '/img/logo/logo-light-streamline.png'}
|
|
|
|
|
|
alt={`${APP_NAME} logo`}
|
|
|
|
|
|
className="h-8 w-8 object-contain"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span className="text-sm font-semibold tracking-wide">{APP_NAME}</span>
|
|
|
|
|
|
|
|
|
|
|
|
<span
|
|
|
|
|
|
className={classNames(
|
|
|
|
|
|
'ml-auto inline-flex items-center gap-2 rounded-full border px-3 py-1 text-xs font-medium uppercase tracking-wider',
|
|
|
|
|
|
isDark
|
|
|
|
|
|
? 'border-amber-400/25 bg-amber-400/10 text-amber-200'
|
|
|
|
|
|
: 'border-amber-500/30 bg-amber-50 text-amber-700',
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<span
|
|
|
|
|
|
className={classNames(
|
|
|
|
|
|
'h-1.5 w-1.5 animate-pulse rounded-full',
|
|
|
|
|
|
isDark ? 'bg-amber-300' : 'bg-amber-500',
|
|
|
|
|
|
)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
Offline
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Gövde */}
|
|
|
|
|
|
<div className="px-6 py-7">
|
|
|
|
|
|
<h5 className={isDark ? 'text-gray-100' : 'text-gray-900'}>
|
|
|
|
|
|
Service temporarily unavailable
|
|
|
|
|
|
</h5>
|
|
|
|
|
|
|
|
|
|
|
|
<p className={classNames('mt-3 text-sm leading-relaxed', isDark ? 'text-gray-400' : 'text-gray-600')}>
|
|
|
|
|
|
The application configuration could not be loaded. The server is running, but it cannot
|
|
|
|
|
|
reach its database right now. Your data is not affected — the application will come back
|
|
|
|
|
|
automatically once the database is available again.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
|
className={classNames(
|
|
|
|
|
|
'mt-5 flex items-center gap-2 rounded-lg border px-3 py-2 text-xs',
|
|
|
|
|
|
isDark
|
|
|
|
|
|
? 'border-gray-700 bg-gray-900 text-gray-400'
|
|
|
|
|
|
: 'border-gray-200 bg-gray-50 text-gray-500',
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<span
|
|
|
|
|
|
className={classNames(
|
|
|
|
|
|
'inline-block h-2 w-2 animate-ping rounded-full',
|
|
|
|
|
|
isDark ? 'bg-sky-400' : 'bg-sky-500',
|
|
|
|
|
|
)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
{attempt > 0
|
|
|
|
|
|
? `Rechecking the connection… (attempt ${attempt})`
|
|
|
|
|
|
: 'Waiting to recheck the connection…'}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Aksiyonlar */}
|
|
|
|
|
|
<div
|
|
|
|
|
|
className={classNames(
|
|
|
|
|
|
'flex flex-col-reverse items-stretch gap-3 border-t px-6 py-4 sm:flex-row sm:items-center sm:justify-between',
|
|
|
|
|
|
isDark ? 'border-gray-700 bg-gray-700' : 'border-gray-200 bg-gray-50',
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<span className={classNames('text-xs', isDark ? 'text-gray-400' : 'text-gray-500')}>
|
|
|
|
|
|
Administrators can run a database migration from the setup page.
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="flex gap-2">
|
|
|
|
|
|
<Button size="sm" onClick={() => window.location.assign(ROUTES_ENUM.setup)}>
|
|
|
|
|
|
Setup page
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button size="sm" variant="solid" onClick={() => window.location.reload()}>
|
|
|
|
|
|
Try again
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export default ServiceUnavailable
|