import { useMemo, useState } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { Link, Navigate, useNavigate } from 'react-router-dom'; import { Check, ChevronRight, Github, Loader2, Rocket, ShieldCheck } from 'lucide-react'; import { useAuthSession } from '@/lib/use-auth-session'; import { completeSetup, createGitHubAppManifest, getSetupStatus, updatePlatformSettings, } from '@/lib/api-client'; import { GhostBtn, QuietBtn } from '@/shared/components/sentry'; // Marker the GitHub App callback reads so the wizard flow resumes here // instead of the settings page. const SETUP_RETURN_KEY = 'containr.setup_return'; type Step = 'account' | 'github' | 'tunnel' | 'done'; export function SetupWizardPage() { const navigate = useNavigate(); const queryClient = useQueryClient(); const sessionQuery = useAuthSession(); const statusQuery = useQuery({ queryKey: ['setup-status'], queryFn: getSetupStatus }); const [override, setOverride] = useState(null); const status = statusQuery.data; const signedIn = Boolean(sessionQuery.data); const step: Step = useMemo(() => { if (override) return override; if (!status) return 'account'; if (!status.has_users || !signedIn) return 'account'; if (!status.has_github_app) return 'github'; if (!status.has_tunnel) return 'tunnel'; return 'done'; }, [override, status, signedIn]); const completeMutation = useMutation({ mutationFn: completeSetup, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['setup-status'] }); navigate('/', { replace: true }); }, }); if (statusQuery.isLoading) { return (
); } // Wizard is optional once the instance is already configured. if (status && !status.needs_setup && !override) { return ; } const steps: { key: Step; label: string }[] = [ { key: 'account', label: 'Admin account' }, { key: 'github', label: 'Git provider' }, { key: 'tunnel', label: 'Public access' }, { key: 'done', label: 'Done' }, ]; const activeIdx = steps.findIndex((s) => s.key === step); return (
{steps.map((s, i) => (
{i < activeIdx ? : i + 1}
{s.label} {i < steps.length - 1 ?
: null}
))}
{step === 'account' ? (

Welcome to Containr

First boot detected. Create the admin account — registration closes to the public right after, so this account owns the instance.

{status?.has_users && !signedIn ? (

An account already exists — sign in to continue setup.

Sign in
) : !status?.has_users ? ( Create admin account ) : null}
) : null} {step === 'github' ? setOverride('tunnel')} /> : null} {step === 'tunnel' ? setOverride('done')} /> : null} {step === 'done' ? (

All set

Deploy your first service from a git repo, an image, or a template — the dashboard is ready.

{completeMutation.isError ? (

{completeMutation.error instanceof Error ? completeMutation.error.message : 'Failed to save setup state'}

) : null} completeMutation.mutate()} disabled={completeMutation.isPending} className="mt-5 !h-10 !px-6" > {completeMutation.isPending ? ( ) : ( )} Enter Containr
) : null}
); } function GitHubStep({ onNext }: { onNext: () => void }) { const queryClient = useQueryClient(); const provisionMutation = useMutation({ mutationFn: async () => { const { url, manifest } = await createGitHubAppManifest({ base_url: window.location.origin, }); sessionStorage.setItem(SETUP_RETURN_KEY, '1'); const form = document.createElement('form'); form.method = 'POST'; form.action = url; const input = document.createElement('input'); input.type = 'hidden'; input.name = 'manifest'; input.value = manifest; form.appendChild(input); document.body.appendChild(form); form.submit(); }, onSuccess: () => queryClient.invalidateQueries({ queryKey: ['setup-status'] }), }); return (

Connect a Git Provider

Provision the instance GitHub App — repo push events then trigger deployments and preview environments automatically. Self-hosted GitLab/Gitea can be added later in Settings.

{provisionMutation.isError ? (

{provisionMutation.error instanceof Error ? provisionMutation.error.message : 'Could not start GitHub App provisioning'}

) : null}
provisionMutation.mutate()} disabled={provisionMutation.isPending} > {provisionMutation.isPending ? : null} Set up GitHub App Skip for now
); } function TunnelStep({ onNext }: { onNext: () => void }) { const queryClient = useQueryClient(); const [token, setToken] = useState(''); const saveMutation = useMutation({ mutationFn: () => updatePlatformSettings({ cloudflareTunnelToken: token.trim() || undefined }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['setup-status'] }); onNext(); }, }); return (

Public Access (optional)

Paste a Cloudflare Tunnel token to expose this instance on the internet. Without one, Containr stays reachable on this network — you can add it later in Settings.

setToken(e.target.value)} placeholder="eyJhIjo… tunnel token" className="mt-4 w-full h-10 px-3 rounded-[var(--radius-md)] border border-[var(--border-subtle)] bg-[var(--surface-muted)] v-mono text-sm text-[var(--text-primary)] placeholder:text-[var(--text-muted)] focus:border-[var(--accent-primary)] transition-colors" /> {saveMutation.isError ? (

{saveMutation.error instanceof Error ? saveMutation.error.message : 'Failed to save token'}

) : null}
(token.trim() ? saveMutation.mutate() : onNext())} disabled={saveMutation.isPending} > {saveMutation.isPending ? : null} {token.trim() ? 'Save tunnel token' : 'Continue'} Skip
); }