'use client' import { useState, useMemo, useEffect, useCallback } from 'react' import Link from 'next/link' import { useParams, useRouter } from 'next/navigation' import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from '@/components/ui/card' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { useOrder, useUpdateOrder, useTriggerProvisioning } from '@/hooks/use-orders' import { useProvisioningLogs, StreamedLog } from '@/hooks/use-provisioning-logs' import { OrderStatus, SubscriptionTier, LogLevel } from '@/types/api' import { ArrowLeft, Globe, User, Server, Clock, CheckCircle, AlertCircle, Loader2, Eye, EyeOff, RefreshCw, ExternalLink, Terminal, Zap, Wifi, WifiOff, } from 'lucide-react' // Status badge component function StatusBadge({ status }: { status: OrderStatus }) { const statusConfig: Record = { PAYMENT_CONFIRMED: { label: 'Payment Confirmed', className: 'bg-blue-100 text-blue-800' }, AWAITING_SERVER: { label: 'Awaiting Server', className: 'bg-yellow-100 text-yellow-800' }, SERVER_READY: { label: 'Server Ready', className: 'bg-purple-100 text-purple-800' }, DNS_PENDING: { label: 'DNS Pending', className: 'bg-orange-100 text-orange-800' }, DNS_READY: { label: 'DNS Ready', className: 'bg-cyan-100 text-cyan-800' }, PROVISIONING: { label: 'Provisioning', className: 'bg-indigo-100 text-indigo-800' }, FULFILLED: { label: 'Fulfilled', className: 'bg-green-100 text-green-800' }, EMAIL_CONFIGURED: { label: 'Complete', className: 'bg-emerald-100 text-emerald-800' }, FAILED: { label: 'Failed', className: 'bg-red-100 text-red-800' }, } const config = statusConfig[status] return ( {config.label} ) } // Server credentials form component function ServerCredentialsForm({ initialIp, initialPort, hasCredentials, onSubmit, isLoading, }: { initialIp?: string initialPort?: number hasCredentials: boolean onSubmit: (ip: string, password: string, port: number) => void isLoading: boolean }) { const [ip, setIp] = useState(initialIp || '') const [password, setPassword] = useState('') const [port, setPort] = useState(String(initialPort || 22)) const [showPassword, setShowPassword] = useState(false) const [testing, setTesting] = useState(false) const [testResult, setTestResult] = useState<{ success: boolean; message: string } | null>(null) const handleTestConnection = async () => { setTesting(true) setTestResult(null) // TODO: Call API to test SSH connection await new Promise(resolve => setTimeout(resolve, 2000)) setTestResult({ success: true, message: 'Connection successful! SSH version: OpenSSH_8.4' }) setTesting(false) } const handleSubmit = (e: React.FormEvent) => { e.preventDefault() onSubmit(ip, password, parseInt(port)) } return ( Server Credentials {hasCredentials ? 'Server credentials have been entered. You can update them if needed.' : 'Enter the server credentials received from the hosting provider.'}
setIp(e.target.value)} required />
setPort(e.target.value)} required />
setPassword(e.target.value)} required={!hasCredentials} className="pr-10" />

Password will be encrypted and deleted after provisioning completes.

{testResult && (
{testResult.success ? ( ) : ( )} {testResult.message}
)}
) } // Provisioning logs component function ProvisioningLogs({ logs, isLive, isConnected, onReconnect }: { logs: Array<{ id: string timestamp: Date level: LogLevel step: string | null message: string }> isLive: boolean isConnected?: boolean onReconnect?: () => void }) { const levelColors: Record = { INFO: 'text-blue-400', WARN: 'text-yellow-400', ERROR: 'text-red-400', DEBUG: 'text-gray-400', } const formatTime = (date: Date) => { const d = new Date(date) return d.toLocaleTimeString('en-US', { hour12: false }) } return (
Provisioning Logs Real-time output from the provisioning process
{isLive && ( <> {isConnected ? (
Live
) : (
Disconnected {onReconnect && ( )}
)} )} {!isLive && logs.length > 0 && ( {logs.length} log{logs.length !== 1 ? 's' : ''} )}
{logs.length === 0 ? (

{isLive && isConnected ? 'Waiting for logs...' : 'No logs available yet.'}

) : ( logs.map((log) => (
[{formatTime(log.timestamp)}]{' '} [{log.level}]{' '} {log.step && [{log.step}]}{' '} {log.message}
)) )}
) } // Order timeline component function OrderTimeline({ status, timestamps, }: { status: OrderStatus timestamps: { createdAt?: Date | null serverReadyAt?: Date | null provisioningStartedAt?: Date | null completedAt?: Date | null } }) { const stages = [ { key: 'payment_confirmed', label: 'Payment Confirmed', status: 'PAYMENT_CONFIRMED' }, { key: 'awaiting_server', label: 'Server Ordered', status: 'AWAITING_SERVER' }, { key: 'server_ready', label: 'Server Ready', status: 'SERVER_READY' }, { key: 'dns_ready', label: 'DNS Configured', status: 'DNS_READY' }, { key: 'provisioning', label: 'Provisioning', status: 'PROVISIONING' }, { key: 'fulfilled', label: 'Fulfilled', status: 'FULFILLED' }, ] const statusOrder = stages.map((s) => s.status) const currentIndex = statusOrder.indexOf(status) const formatDate = (date: Date | null | undefined) => { if (!date) return null return new Date(date).toLocaleString('en-US', { month: 'short', day: 'numeric', year: 'numeric', hour: 'numeric', minute: '2-digit', hour12: true, }) } const getTimestamp = (key: string): string | null => { switch (key) { case 'payment_confirmed': return formatDate(timestamps.createdAt) case 'server_ready': return formatDate(timestamps.serverReadyAt) case 'provisioning': return formatDate(timestamps.provisioningStartedAt) case 'fulfilled': return formatDate(timestamps.completedAt) default: return null } } return ( Order Timeline
{stages.map((stage, index) => { const isComplete = index < currentIndex || (index === currentIndex && status !== 'FAILED') const isCurrent = index === currentIndex const timestamp = getTimestamp(stage.key) return (
{isComplete ? ( ) : ( {index + 1} )}
{index < stages.length - 1 && (
)}

{stage.label}

{timestamp && (

{timestamp}

)}
) })}
) } export default function OrderDetailPage() { const params = useParams() const router = useRouter() const orderId = params.id as string // Fetch order data const { data: order, isLoading, isError, error, refetch, isFetching, } = useOrder(orderId) // Mutations const updateOrder = useUpdateOrder() const triggerProvision = useTriggerProvisioning() // Check if we should enable SSE streaming const isProvisioning = order?.status === OrderStatus.PROVISIONING // SSE for live log streaming const { logs: streamedLogs, isConnected, isComplete, reconnect, } = useProvisioningLogs({ orderId, enabled: isProvisioning, onStatusChange: useCallback((newStatus: OrderStatus) => { // Refetch order data when status changes refetch() }, [refetch]), onComplete: useCallback((success: boolean) => { // Refetch order data when provisioning completes refetch() }, [refetch]), }) // Computed values const tierLabel = useMemo(() => { if (!order) return '' return order.tier === SubscriptionTier.HUB_DASHBOARD ? 'Hub Dashboard' : 'Control Panel' }, [order?.tier]) // Merge historical logs with streamed logs, avoiding duplicates const allLogs = useMemo(() => { const historicalLogs = order?.provisioningLogs || [] const historicalLogIds = new Set(historicalLogs.map(l => l.id)) // Add only new streamed logs that aren't in historical const newStreamedLogs = streamedLogs.filter(l => !historicalLogIds.has(l.id)) // Combine and sort by timestamp const combined = [ ...historicalLogs.map(l => ({ id: l.id, level: l.level, step: l.step, message: l.message, timestamp: new Date(l.timestamp), })), ...newStreamedLogs, ] return combined.sort( (a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime() ) }, [order?.provisioningLogs, streamedLogs]) // Auto-scroll logs to bottom when new logs come in useEffect(() => { if (isProvisioning && allLogs.length > 0) { const container = document.getElementById('log-container') if (container) { container.scrollTop = container.scrollHeight } } }, [allLogs.length, isProvisioning]) const handleCredentialsSubmit = async (ip: string, password: string, port: number) => { await updateOrder.mutateAsync({ id: orderId, data: { serverIp: ip, serverPassword: password, sshPort: port, }, }) } const handleTriggerProvisioning = async () => { try { await triggerProvision.mutateAsync(orderId) } catch (err) { console.error('Failed to trigger provisioning:', err) } } // Loading state if (isLoading) { return (

Loading order details...

) } // Error state if (isError || !order) { return (

Failed to load order

{error instanceof Error ? error.message : 'Order not found'}

) } const showCredentialsForm = order.status === OrderStatus.AWAITING_SERVER || order.status === OrderStatus.SERVER_READY const showProvisionButton = order.status === OrderStatus.DNS_READY || order.status === OrderStatus.FAILED const showLogs = order.status === OrderStatus.PROVISIONING || order.status === OrderStatus.FULFILLED || order.status === OrderStatus.EMAIL_CONFIGURED || order.status === OrderStatus.FAILED return (
{/* Header */}

{order.domain}

Order #{orderId.slice(0, 8)}...

{showProvisionButton && ( )} {order.portainerUrl && ( )} {order.dashboardUrl && ( )}
{/* Failure reason banner */} {order.status === OrderStatus.FAILED && order.failureReason && (

Provisioning Failed

{order.failureReason}

)} {/* Order info cards */}
Customer

{order.user.name || order.user.company || 'N/A'}

{order.user.email}

{order.user.company && order.user.name && (

{order.user.company}

)}
Domain & Tier

{order.domain}

{tierLabel}
Server {order.serverIp ? ( <>

{order.serverIp}

SSH Port: {order.sshPort || 22}

) : (

Not configured

)}
{/* Tools list */} Selected Tools Tools to be deployed on this server
{order.tools.map((tool) => ( {tool} ))}
{/* Server credentials form (show for AWAITING_SERVER or SERVER_READY status) */} {showCredentialsForm && ( )} {/* Two column layout for timeline and logs */}
{/* Show logs for provisioning/completed/failed status */} {showLogs && ( )}
{/* Jobs history */} {order.jobs && order.jobs.length > 0 && ( Job History Recent provisioning job attempts
{order.jobs.map((job) => (

Attempt {job.attempt} of {job.maxAttempts}

{new Date(job.createdAt).toLocaleString()}

{job.status} {job.error && (

{job.error}

)}
))}
)}
) }