import { useState, useEffect, useCallback, useRef } from 'react'
import { useNavigate } from '@tanstack/react-router'
import { useQueryClient } from '@tanstack/react-query'
import { CheckCircle2, Loader2, AlertTriangle, Router } from 'lucide-react'
import { tenantsApi, devicesApi } from '@/lib/api'
import type { TenantResponse, DeviceResponse } from '@/lib/api'
import { toast } from '@/components/ui/toast'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
// Checkbox removed -- TLS is always on (port 8729)
type Step = 1 | 2 | 3 | 'complete'
const STEP_LABELS = ['Create Organization', 'Add Device', 'Verify']
const POLL_INTERVAL = 2000
const POLL_TIMEOUT = 120000
// ---- Step Indicator ----
function StepIndicator({ currentStep }: { currentStep: Step }) {
const stepNum = currentStep === 'complete' ? 4 : currentStep
return (
{STEP_LABELS.map((label, idx) => {
const num = idx + 1
const isCompleted = stepNum > num
const isCurrent = stepNum === num
return (
{idx > 0 && (
)}
{isCompleted ? (
) : (
num
)}
{label}
)
})}
)
}
// ---- Step 1: Create Tenant ----
interface Step1Props {
onComplete: (tenant: TenantResponse) => void
}
function CreateTenantStep({ onComplete }: Step1Props) {
const [name, setName] = useState('')
const [contactEmail, setContactEmail] = useState('')
const [isSubmitting, setIsSubmitting] = useState(false)
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!name.trim()) return
setIsSubmitting(true)
try {
const tenant = await tenantsApi.create({
name: name.trim(),
contact_email: contactEmail.trim() || undefined,
})
toast({ title: `Organization "${tenant.name}" created` })
onComplete(tenant)
} catch (err: unknown) {
const detail =
(err as { response?: { data?: { detail?: string } } })?.response?.data
?.detail ?? 'Failed to create organization'
toast({ title: detail, variant: 'destructive' })
} finally {
setIsSubmitting(false)
}
}
return (
)
}
// ---- Step 2: Add Device ----
interface Step2Props {
tenantId: string
onComplete: (device: DeviceResponse) => void
onSkip: () => void
}
function AddDeviceStep({ tenantId, onComplete, onSkip }: Step2Props) {
const [form, setForm] = useState({
hostname: '',
ip_address: '',
api_ssl_port: '8729',
username: '',
password: '',
})
const [isSubmitting, setIsSubmitting] = useState(false)
const update =
(field: keyof typeof form) =>
(e: React.ChangeEvent) => {
setForm((f) => ({ ...f, [field]: e.target.value }))
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!form.ip_address.trim() || !form.username.trim() || !form.password.trim()) {
toast({ title: 'IP address, username, and password are required', variant: 'destructive' })
return
}
setIsSubmitting(true)
try {
const device = await devicesApi.create(tenantId, {
hostname: form.hostname.trim() || form.ip_address.trim(),
ip_address: form.ip_address.trim(),
api_ssl_port: parseInt(form.api_ssl_port) || 8729,
username: form.username.trim(),
password: form.password,
})
toast({ title: `Device "${device.hostname}" added` })
onComplete(device)
} catch (err: unknown) {
const detail =
(err as { response?: { data?: { detail?: string } } })?.response?.data
?.detail ?? 'Failed to add device. Check the connection details.'
toast({ title: detail, variant: 'destructive' })
} finally {
setIsSubmitting(false)
}
}
return (
)
}
// ---- Step 3: Verify Connectivity ----
interface Step3Props {
tenantId: string
deviceId: string
onComplete: () => void
}
function VerifyConnectivityStep({ tenantId, deviceId, onComplete }: Step3Props) {
const [status, setStatus] = useState<'polling' | 'online' | 'timeout'>('polling')
const intervalRef = useRef | null>(null)
const timeoutRef = useRef | null>(null)
const cleanup = useCallback(() => {
if (intervalRef.current) {
clearInterval(intervalRef.current)
intervalRef.current = null
}
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
timeoutRef.current = null
}
}, [])
const startPolling = useCallback(() => {
setStatus('polling')
cleanup()
const poll = async () => {
try {
const device = await devicesApi.get(tenantId, deviceId)
if (device.status === 'online') {
cleanup()
setStatus('online')
}
} catch {
// Device might not be reachable yet -- continue polling
}
}
// Poll immediately then on interval
void poll()
intervalRef.current = setInterval(() => void poll(), POLL_INTERVAL)
// Timeout after POLL_TIMEOUT ms
timeoutRef.current = setTimeout(() => {
if (intervalRef.current) {
clearInterval(intervalRef.current)
intervalRef.current = null
}
setStatus((prev) => (prev === 'polling' ? 'timeout' : prev))
}, POLL_TIMEOUT)
}, [tenantId, deviceId, cleanup])
useEffect(() => {
startPolling()
return cleanup
}, [startPolling, cleanup])
return (
{status === 'polling' && (
<>
Verifying connection...
This typically takes 1-2 minutes while the poller connects to your device.
>
)}
{status === 'online' && (
<>
Device connected successfully!
Your MikroTik device is online and ready to manage.
>
)}
{status === 'timeout' && (
<>
Device hasn't connected yet
It may take a few more moments for the poller to reach your device. You can
wait longer or continue to the dashboard.
>
)}
)
}
// ---- Main SetupWizard ----
export function SetupWizard() {
const navigate = useNavigate()
const queryClient = useQueryClient()
const [step, setStep] = useState(1)
const [tenantId, setTenantId] = useState(null)
const [deviceId, setDeviceId] = useState(null)
// On mount, check if a tenant already exists (e.g. user skipped step 2 and got redirected back)
useEffect(() => {
let cancelled = false
tenantsApi.list().then((tenants) => {
// Filter out System (Internal) tenant — only real customer tenants count
const realTenants = tenants.filter(
(t) => t.id !== '00000000-0000-0000-0000-000000000000',
)
if (!cancelled && realTenants.length > 0 && !tenantId) {
setTenantId(realTenants[0].id)
setStep(2)
}
}).catch(() => {})
return () => { cancelled = true }
}, []) // eslint-disable-line react-hooks/exhaustive-deps
const goToDashboard = useCallback(() => {
void queryClient.invalidateQueries({ queryKey: ['tenants'] })
void navigate({ to: '/' })
}, [navigate, queryClient])
return (
{/* Logo / Title */}
TOD - The Other Dude
First-time Setup
{/* Step Indicator */}
{/* Card */}
{step === 1 && (
{
void queryClient.invalidateQueries({ queryKey: ['tenants'] })
setTenantId(tenant.id)
setStep(2)
}}
/>
)}
{step === 2 && tenantId && (
{
setDeviceId(device.id)
setStep(3)
}}
onSkip={goToDashboard}
/>
)}
{step === 3 && tenantId && deviceId && (
)}
)
}