2026-04-23 10:36:51 +00:00
|
|
|
|
import { useEffect, useRef, useState } from 'react'
|
|
|
|
|
|
import { APP_NAME } from '@/constants/app.constant'
|
|
|
|
|
|
import { getMigrateUrl } from '@/services/setup.service'
|
|
|
|
|
|
import { applicationConfigurationUrl, getAppConfig } 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<string, string> = {
|
|
|
|
|
|
info: 'text-gray-300',
|
|
|
|
|
|
warn: 'text-yellow-400',
|
|
|
|
|
|
error: 'text-red-400',
|
|
|
|
|
|
success: 'text-green-400',
|
|
|
|
|
|
restart: 'text-blue-400',
|
|
|
|
|
|
done: 'text-blue-400',
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const DatabaseSetup = () => {
|
|
|
|
|
|
const [logs, setLogs] = useState<LogLine[]>([])
|
|
|
|
|
|
const [status, setStatus] = useState<MigrationStatus>('idle')
|
|
|
|
|
|
const [pollCountdown, setPollCountdown] = useState(0)
|
|
|
|
|
|
const logEndRef = useRef<HTMLDivElement>(null)
|
|
|
|
|
|
const eventSourceRef = useRef<EventSource | null>(null)
|
|
|
|
|
|
const pollTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
|
|
|
|
|
|
|
|
|
|
// Auto-scroll to bottom when new logs arrive
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
logEndRef.current?.scrollIntoView({ behavior: 'smooth' })
|
|
|
|
|
|
}, [logs])
|
|
|
|
|
|
|
|
|
|
|
|
// Cleanup on component unmount
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
eventSourceRef.current?.close()
|
|
|
|
|
|
if (pollTimerRef.current) clearTimeout(pollTimerRef.current)
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
|
|
const addLog = (level: LogLine['level'], message: string) => {
|
|
|
|
|
|
setLogs((prev) => [...prev, { level, message }])
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Poll ABP config endpoint until server is ready ──────────────
|
|
|
|
|
|
const pollUntilReady = () => {
|
|
|
|
|
|
setStatus('restarting')
|
|
|
|
|
|
|
|
|
|
|
|
let attempt = 0
|
2026-04-23 12:30:20 +00:00
|
|
|
|
let successCount = 0
|
|
|
|
|
|
const REQUIRED_SUCCESS = 2 // sunucunun kararlı olduğunu doğrulamak için arka arkaya 2 başarılı yanıt
|
2026-04-23 10:36:51 +00:00
|
|
|
|
|
|
|
|
|
|
const tick = async () => {
|
|
|
|
|
|
attempt++
|
|
|
|
|
|
setPollCountdown(attempt)
|
|
|
|
|
|
try {
|
2026-04-23 12:30:20 +00:00
|
|
|
|
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()
|
|
|
|
|
|
// ABP config yanıtının geçerli olduğunu doğrula (currentUser alanı her zaman bulunur)
|
|
|
|
|
|
if (json && typeof json.currentUser === 'object') {
|
|
|
|
|
|
successCount++
|
|
|
|
|
|
if (successCount >= REQUIRED_SUCCESS) {
|
|
|
|
|
|
// Sunucu tamamen hazır — yönlendir
|
|
|
|
|
|
window.location.href = '/'
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
// Bir sonraki doğrulama denemesi
|
|
|
|
|
|
pollTimerRef.current = setTimeout(tick, 1000)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
// JSON parse hatası — sunucu henüz tam hazır değil
|
|
|
|
|
|
}
|
2026-04-23 10:36:51 +00:00
|
|
|
|
}
|
2026-04-23 12:30:20 +00:00
|
|
|
|
// Başarısız — sıfırla ve tekrar dene
|
|
|
|
|
|
successCount = 0
|
2026-04-23 10:36:51 +00:00
|
|
|
|
} catch {
|
2026-04-23 12:30:20 +00:00
|
|
|
|
// Sunucu henüz yanıt vermiyor — bekleniyor, tekrar dene
|
|
|
|
|
|
successCount = 0
|
2026-04-23 10:36:51 +00:00
|
|
|
|
}
|
|
|
|
|
|
pollTimerRef.current = setTimeout(tick, 2000)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// İlk denemeden önce kısa bir bekleme (sunucunun kapanma süresi)
|
2026-04-23 12:30:20 +00:00
|
|
|
|
pollTimerRef.current = setTimeout(tick, 3000)
|
2026-04-23 10:36:51 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const startMigration = () => {
|
|
|
|
|
|
if (status === 'running') return
|
|
|
|
|
|
|
|
|
|
|
|
setLogs([])
|
|
|
|
|
|
setStatus('running')
|
|
|
|
|
|
addLog('info', 'Starting migration...')
|
|
|
|
|
|
|
|
|
|
|
|
const url = getMigrateUrl()
|
|
|
|
|
|
const es = new EventSource(url)
|
|
|
|
|
|
eventSourceRef.current = es
|
|
|
|
|
|
|
|
|
|
|
|
es.onmessage = (event) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const data = JSON.parse(event.data) as { level?: string; message?: string }
|
|
|
|
|
|
const level = (data.level ?? 'info') as LogLine['level']
|
|
|
|
|
|
const message = data.message ?? event.data
|
|
|
|
|
|
|
|
|
|
|
|
if (level === 'done') {
|
|
|
|
|
|
es.close()
|
|
|
|
|
|
eventSourceRef.current = null
|
|
|
|
|
|
// If we received a "restart" event, switch to poll mode; otherwise keep success/error state
|
|
|
|
|
|
setStatus((prev) => {
|
|
|
|
|
|
if (prev === 'running') return 'error' // failed — no restart event received
|
|
|
|
|
|
return prev
|
|
|
|
|
|
})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
addLog(level, message)
|
|
|
|
|
|
|
|
|
|
|
|
if (level === 'success') {
|
|
|
|
|
|
setStatus('success')
|
|
|
|
|
|
} else if (level === 'error') {
|
|
|
|
|
|
setStatus('error')
|
|
|
|
|
|
} else if (level === 'restart') {
|
|
|
|
|
|
// Backend is stopping the minimal app — start polling
|
|
|
|
|
|
pollUntilReady()
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
addLog('info', event.data)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
es.onerror = () => {
|
|
|
|
|
|
es.close()
|
|
|
|
|
|
eventSourceRef.current = null
|
|
|
|
|
|
setStatus((prev) => (prev === 'running' ? 'error' : prev))
|
|
|
|
|
|
addLog('error', 'Server connection lost or migration could not be completed.')
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="min-h-screen bg-gray-900 flex flex-col items-center justify-center p-6 text-white">
|
|
|
|
|
|
{/* Header */}
|
|
|
|
|
|
<div className="mb-8 text-center">
|
|
|
|
|
|
<h1 className="text-3xl font-bold mb-2">{APP_NAME}</h1>
|
|
|
|
|
|
<p className="text-gray-400 text-lg">Initial Setup — Creating Database</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Status Card */}
|
|
|
|
|
|
<div className="w-full max-w-4xl bg-gray-800 rounded-xl shadow-2xl overflow-hidden">
|
|
|
|
|
|
{/* Card Header */}
|
|
|
|
|
|
<div className="flex items-center justify-between px-5 py-4 bg-gray-700 border-b border-gray-600">
|
|
|
|
|
|
<div className="flex items-center gap-3">
|
|
|
|
|
|
<StatusIndicator status={status} />
|
|
|
|
|
|
<span className="font-semibold text-sm tracking-wide">
|
|
|
|
|
|
{status === 'idle' && 'Ready to Setup'}
|
|
|
|
|
|
{status === 'running' && 'Migration Running...'}
|
|
|
|
|
|
{status === 'success' && 'Migration Completed ✓'}
|
|
|
|
|
|
{status === 'restarting' && `Server Restarting... (attempt ${pollCountdown})`}
|
|
|
|
|
|
{status === 'error' && 'Migration Failed ✗'}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<span className="text-xs text-gray-400">{logs.length} log lines</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Log Terminal */}
|
|
|
|
|
|
<div className="h-96 overflow-y-auto bg-gray-950 px-4 py-3 font-mono text-xs leading-relaxed">
|
|
|
|
|
|
{logs.length === 0 ? (
|
|
|
|
|
|
<p className="text-gray-600 mt-2">Logs will appear here when migration starts...</p>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
logs.map((line, i) => (
|
|
|
|
|
|
<div key={i} className={`${levelClass[line.level] ?? 'text-gray-300'} mb-0.5`}>
|
|
|
|
|
|
<span className="text-gray-600 mr-2 select-none">{String(i + 1).padStart(4, '0')}</span>
|
|
|
|
|
|
{line.message}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))
|
|
|
|
|
|
)}
|
|
|
|
|
|
<div ref={logEndRef} />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Action Area */}
|
|
|
|
|
|
<div className="flex items-center justify-between px-5 py-4 bg-gray-700 border-t border-gray-600">
|
|
|
|
|
|
<div className="text-xs text-gray-400">
|
|
|
|
|
|
{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.'}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="flex gap-3">
|
|
|
|
|
|
{(status === 'idle' || status === 'error') && (
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={startMigration}
|
|
|
|
|
|
className="px-4 py-2 bg-indigo-600 hover:bg-indigo-500 text-white text-sm font-medium rounded-lg transition-colors"
|
|
|
|
|
|
>
|
|
|
|
|
|
{status === 'error' ? 'Retry' : 'Start Setup'}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{status === 'restarting' && (
|
|
|
|
|
|
<div className="flex items-center gap-2 text-blue-400 text-sm">
|
|
|
|
|
|
<span className="inline-block w-3 h-3 rounded-full bg-blue-400 animate-ping" />
|
|
|
|
|
|
Waiting...
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<p className="mt-6 text-xs text-gray-600">
|
|
|
|
|
|
This page is only visible when the database does not exist.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ─── Status Indicator ────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
const StatusIndicator = ({ status }: { status: MigrationStatus }) => {
|
|
|
|
|
|
const cls: Record<MigrationStatus, string> = {
|
|
|
|
|
|
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 <span className={`inline-block w-3 h-3 rounded-full ${cls[status]}`} />
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export default DatabaseSetup
|
|
|
|
|
|
|
|
|
|
|
|
|