import { useEffect, useRef, useState } from 'react' import { APP_NAME } from '@/constants/app.constant' import { getMigrateUrl } from '@/services/setup.service' import { applicationConfigurationUrl } from '@/services/abpConfig.service' interface LogLine { level: 'info' | 'warn' | 'error' | 'success' | 'restart' | 'done' message: string } type MigrationStatus = 'idle' | 'running' | 'success' | 'error' | 'restarting' const levelClass: Record = { info: 'text-gray-300', warn: 'text-yellow-400', error: 'text-red-400', success: 'text-green-400', restart: 'text-blue-400', done: 'text-blue-400', } /** * Sunucu yeniden başlayana kadar ABP config endpoint'ini poll eder. * Arka arkaya 2 başarılı yanıt alındığında onReady çağrılır. * @param onReady Sunucu hazır olduğunda çağrılacak callback * @param onAttempt Her denemede kaçıncı deneme olduğunu bildiren opsiyonel callback */ const pollUntilServerReady = (onReady: () => void, onAttempt?: (attempt: number) => void): (() => void) => { const REQUIRED_SUCCESS = 2 let attempt = 0 let successCount = 0 let timerId: ReturnType | null = null let cancelled = false const tick = async () => { if (cancelled) return attempt++ onAttempt?.(attempt) try { const res = await fetch( `${import.meta.env.VITE_API_URL ?? ''}${applicationConfigurationUrl(false)}`, { method: 'GET', headers: { Accept: 'application/json' }, cache: 'no-store' }, ) if (res.status === 200) { try { const json = await res.json() if (json && typeof json.currentUser === 'object') { successCount++ if (successCount >= REQUIRED_SUCCESS) { onReady() return } timerId = setTimeout(tick, 1000) return } } catch { /* parse hatası */ } } successCount = 0 } catch { successCount = 0 } timerId = setTimeout(tick, 2000) } // İlk denemeden önce kısa bekleme (sunucunun kapanma süresi) timerId = setTimeout(tick, 3000) // İptal fonksiyonu döner return () => { cancelled = true if (timerId) clearTimeout(timerId) } } const DatabaseSetup = () => { const [logs, setLogs] = useState([]) const [status, setStatus] = useState('idle') const [pollCountdown, setPollCountdown] = useState(0) const logEndRef = useRef(null) const pollTimerRef = useRef | null>(null) // Auto-scroll to bottom when new logs arrive useEffect(() => { logEndRef.current?.scrollIntoView({ behavior: 'smooth' }) }, [logs]) // Cleanup on component unmount useEffect(() => { return () => { const ref = pollTimerRef.current as any if (ref?.unref) ref.unref() else if (pollTimerRef.current) clearTimeout(pollTimerRef.current as any) } }, []) const addLog = (level: LogLine['level'], message: string) => { setLogs((prev) => [...prev, { level, message }]) } // ── Poll ABP config endpoint until server is ready ────────────── const pollUntilReady = () => { setStatus('restarting') const cancel = pollUntilServerReady( () => { window.location.href = '/' }, (attempt) => setPollCountdown(attempt), ) pollTimerRef.current = { unref: cancel } as any } const startMigration = async () => { if (status === 'running') return setLogs([]) setStatus('running') addLog('info', 'Starting migration...') const url = getMigrateUrl() const abortController = new AbortController() const parseChunk = (chunk: string) => { // SSE format: "data: {...}\n\n" const lines = chunk.split('\n') for (const line of lines) { const trimmed = line.trim() if (!trimmed.startsWith('data:')) continue const raw = trimmed.slice(5).trim() if (!raw) continue try { const data = JSON.parse(raw) as { level?: string; message?: string } const level = (data.level ?? 'info') as LogLine['level'] const message = data.message ?? raw if (level === 'done') { setStatus((prev) => { if (prev === 'running') return 'error' return prev }) return } addLog(level, message) if (level === 'success') { setStatus('success') } else if (level === 'error') { setStatus('error') } else if (level === 'restart') { pollUntilReady() } } catch { addLog('info', raw) } } } try { const response = await fetch(url, { method: 'POST', headers: { Accept: 'text/event-stream' }, signal: abortController.signal, }) if (!response.ok || !response.body) { addLog('error', `Server responded with status ${response.status}`) setStatus('error') return } const reader = response.body.getReader() const decoder = new TextDecoder() // eslint-disable-next-line no-constant-condition while (true) { const { done, value } = await reader.read() if (done) break parseChunk(decoder.decode(value, { stream: true })) } } catch (err: any) { if (err?.name !== 'AbortError') { setStatus((prev) => (prev === 'running' ? 'error' : prev)) addLog('error', 'Server connection lost or migration could not be completed.') } } } return (
{/* Header */}

{APP_NAME}

Initial Setup — Creating Database

{/* Status Card */}
{/* Card Header */}
{status === 'idle' && 'Ready to Setup'} {status === 'running' && 'Migration Running...'} {status === 'success' && 'Migration Completed ✓'} {status === 'restarting' && `Server Restarting... (attempt ${pollCountdown})`} {status === 'error' && 'Migration Failed ✗'}
{logs.length} log lines
{/* Log Terminal */}
{logs.length === 0 ? (

Logs will appear here when migration starts...

) : ( logs.map((line, i) => (
{String(i + 1).padStart(4, '0')} {line.message}
)) )}
{/* Action Area */}
{status === 'idle' && 'Database not found. Press the button to start migration.'} {status === 'running' && 'Please wait, migration is in progress...'} {status === 'success' && 'Migration completed. Server is restarting...'} {status === 'restarting' && 'Waiting for application server to be ready, you will be redirected automatically...'} {status === 'error' && 'Review the errors in the logs, fix the issue, and try again.'}
{(status === 'idle' || status === 'error') && ( )} {status === 'restarting' && (
Waiting...
)}

This page is only visible when the database does not exist.

) } // ─── Status Indicator ──────────────────────────────────────────────────────── const StatusIndicator = ({ status }: { status: MigrationStatus }) => { const cls: Record = { idle: 'bg-gray-500', running: 'bg-yellow-400 animate-pulse', success: 'bg-green-500', restarting: 'bg-blue-400 animate-ping', error: 'bg-red-500', } return } export default DatabaseSetup