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 (

Create your first organization

Organizations group devices by client or location.

setName(e.target.value)} placeholder="Acme Corporation" autoFocus />
setContactEmail(e.target.value)} placeholder="admin@acme.com (optional)" />
) } // ---- 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 (

Add your first MikroTik device

Enter the RouterOS device connection details.

) } // ---- 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 && ( )}
) }