Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified public/clawbox-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
54 changes: 39 additions & 15 deletions src/components/CredentialsStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ interface CredentialsStepProps {
onNext: () => void;
}

// Cumulative ~52 s covers the Jetson recovery window (10–20 s) plus
// headroom for DHCP-rotated IPs after a hostname change.
const MDNS_PROBE_BACKOFF_MS = [3_000, 5_000, 7_000, 10_000, 12_000, 15_000] as const;
const MDNS_PROBE_TIMEOUT_MS = 10_000;

export default function CredentialsStep({ onNext }: CredentialsStepProps) {
const { t } = useT();
const [password, setPassword] = useState("");
Expand Down Expand Up @@ -191,23 +196,42 @@ export default function CredentialsStep({ onNext }: CredentialsStepProps) {
} catch (err) {
if (err instanceof DOMException && err.name === "AbortError") return;
if (err instanceof TypeError && err.message.includes("fetch") && isAllowedRedirect) {
// Connection dropped mid-save — most likely the hostname just changed
// and mDNS is pointing at the new name. Probe the new URL before
// redirecting so we don't strand the user on a dead hostname.
try {
await fetch(newSetupUrl.toString(), {
method: "HEAD",
signal: AbortSignal.timeout(5000),
});
window.location.replace(newSetupUrl.toString());
return;
} catch {
setStatus({
type: "error",
message: `Could not reach ${newSetupUrl.toString()} — settings were saved, but mDNS may not be ready. Try opening the URL manually.`,
// Settings are saved server-side; the hotspot reconfig or hostname
// change just dropped this tab. Show a soft "reconnecting" toast
// and probe the new URL until it answers — only flip to red after
// the full backoff exhausts.
setStatus({
type: "success",
message: `Settings saved. Reconnecting to ${newSetupUrl.toString()}…`,
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
for (const delayMs of MDNS_PROBE_BACKOFF_MS) {
if (controller.signal.aborted) return;
await new Promise<void>((resolve) => {
if (controller.signal.aborted) { resolve(); return; }
const timer = setTimeout(resolve, delayMs);
controller.signal.addEventListener("abort", () => {
clearTimeout(timer);
resolve();
}, { once: true });
});
return;
if (controller.signal.aborted) return;
try {
await fetch(newSetupUrl.toString(), {
method: "HEAD",
cache: "no-store",
signal: AbortSignal.timeout(MDNS_PROBE_TIMEOUT_MS),
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
window.location.replace(newSetupUrl.toString());
return;
} catch {
// continue backoff
}
}
setStatus({
type: "error",
message: `Settings were saved, but ${newSetupUrl.toString()} is still unreachable after ~1 minute. Open the URL manually to continue setup.`,
});
return;
}
setStatus({
type: "error",
Expand Down
47 changes: 42 additions & 5 deletions src/components/SetupWizard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,10 @@ interface SetupWizardProps {

function SetupWizardInner({ onComplete }: SetupWizardProps = {}) {
const { t } = useT();
// Hold a live reference to t so the completion effect can translate without
// re-running (and re-POSTing) on every locale change.
const tRef = useRef(t);
tRef.current = t;
const [currentStep, setCurrentStep] = useState(1);
const [isLoading, setIsLoading] = useState(true);
const [setupError, setSetupError] = useState<string | null>(null);
Expand Down Expand Up @@ -387,9 +391,31 @@ function SetupWizardInner({ onComplete }: SetupWizardProps = {}) {
return false;
}

async function postCompleteWithRetry(): Promise<Response> {
// The POST can fail transiently if the gateway restart from the AI
// step is still settling, or the browser's connection is briefly
// degraded after the hotspot reconfig in step 3.
const backoffs = [0, 2_000, 4_000];
let lastErr: unknown = null;
for (const delayMs of backoffs) {
if (cancelled) throw new Error("cancelled");
if (delayMs > 0) await delay(delayMs);
if (cancelled) throw new Error("cancelled");
try {
return await fetch("/setup-api/setup/complete", {
method: "POST",
signal: AbortSignal.timeout(15_000),
});
} catch (err) {
lastErr = err;
}
}
throw lastErr instanceof Error ? lastErr : new Error("Failed to complete setup");
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

async function runCompletion() {
try {
const res = await fetch("/setup-api/setup/complete", { method: "POST" });
const res = await postCompleteWithRetry();
if (!res.ok) {
throw new Error(await extractErrorMessage(res, `Failed to complete setup (${res.status})`));
}
Expand Down Expand Up @@ -417,7 +443,11 @@ function SetupWizardInner({ onComplete }: SetupWizardProps = {}) {
setCompletionStarted(false);
setCompletionComplete(false);
setCompletionPhase(0);
setCompletionError(err instanceof Error ? err.message : "Failed to finish setup");
const fallback = tRef.current("wizard.completionUnreachable");
const isNetworkError = err instanceof TypeError;
setCompletionError(
isNetworkError || !(err instanceof Error) ? fallback : err.message
);
}
}

Expand Down Expand Up @@ -466,12 +496,12 @@ function SetupWizardInner({ onComplete }: SetupWizardProps = {}) {
className="w-8 h-8 sm:w-9 sm:h-9 object-contain"
priority
/>
<div className="hidden sm:flex flex-col leading-tight">
<div className="hidden sm:flex items-baseline gap-1.5">
<span className="text-xl font-bold font-display title-gradient">
ClawBox
</span>
<span className="text-[10px] text-green-400 -mt-1">
{process.env.NEXT_PUBLIC_APP_VERSION}
<span className="font-mono text-xs text-[var(--text-secondary)] tabular-nums">
<span className="text-[var(--text-muted)] opacity-60 mx-1">/</span>{process.env.NEXT_PUBLIC_APP_VERSION}
</span>
</div>
</Link>
Expand Down Expand Up @@ -513,6 +543,13 @@ function SetupWizardInner({ onComplete }: SetupWizardProps = {}) {
{completionError && currentStep === 5 && (
<div className="w-full max-w-[520px] mb-4">
<StatusMessage type="error" message={completionError} />
<button
type="button"
onClick={startCompletion}
className="mt-3 px-5 py-2 btn-gradient text-white rounded-lg text-sm font-semibold cursor-pointer transition transform hover:scale-105"
>
{t("retry")}
</button>
</div>
)}
{currentStep === 1 && (
Expand Down
10 changes: 10 additions & 0 deletions src/lib/translations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ const setupTranslations: Record<Locale, Record<string, string>> = {
"wizard.restarting": "Restarting...",
"wizard.shutdownConfirm": "Shut down device?",
"wizard.restartConfirm": "Restart device?",
"wizard.completionUnreachable": "Couldn't reach the device. Please check your connection and try again.",

// === ProgressBar ===
"progress.wifi": "WiFi",
Expand Down Expand Up @@ -615,6 +616,7 @@ const setupTranslations: Record<Locale, Record<string, string>> = {
"wizard.restarting": "Рестартиране...",
"wizard.shutdownConfirm": "Изключване на устройството?",
"wizard.restartConfirm": "Рестартиране на устройството?",
"wizard.completionUnreachable": "Не може да се свърже с устройството. Проверете връзката и опитайте отново.",

// === ProgressBar ===
"progress.wifi": "WiFi",
Expand Down Expand Up @@ -996,6 +998,7 @@ const setupTranslations: Record<Locale, Record<string, string>> = {
"wizard.restarting": "Wird neu gestartet...",
"wizard.shutdownConfirm": "Gerät ausschalten?",
"wizard.restartConfirm": "Gerät neu starten?",
"wizard.completionUnreachable": "Gerät konnte nicht erreicht werden. Bitte überprüfen Sie Ihre Verbindung und versuchen Sie es erneut.",

// === ProgressBar ===
"progress.wifi": "WLAN",
Expand Down Expand Up @@ -1377,6 +1380,7 @@ const setupTranslations: Record<Locale, Record<string, string>> = {
"wizard.restarting": "Reiniciando...",
"wizard.shutdownConfirm": "¿Apagar el dispositivo?",
"wizard.restartConfirm": "¿Reiniciar el dispositivo?",
"wizard.completionUnreachable": "No se pudo conectar con el dispositivo. Comprueba tu conexión e inténtalo de nuevo.",

// === ProgressBar ===
"progress.wifi": "WiFi",
Expand Down Expand Up @@ -1758,6 +1762,7 @@ const setupTranslations: Record<Locale, Record<string, string>> = {
"wizard.restarting": "Redémarrage...",
"wizard.shutdownConfirm": "Éteindre l'appareil ?",
"wizard.restartConfirm": "Redémarrer l'appareil ?",
"wizard.completionUnreachable": "Impossible de joindre l'appareil. Veuillez vérifier votre connexion et réessayer.",

// === ProgressBar ===
"progress.wifi": "WiFi",
Expand Down Expand Up @@ -2139,6 +2144,7 @@ const setupTranslations: Record<Locale, Record<string, string>> = {
"wizard.restarting": "Riavvio...",
"wizard.shutdownConfirm": "Spegnere il dispositivo?",
"wizard.restartConfirm": "Riavviare il dispositivo?",
"wizard.completionUnreachable": "Impossibile raggiungere il dispositivo. Controlla la connessione e riprova.",

// === ProgressBar ===
"progress.wifi": "WiFi",
Expand Down Expand Up @@ -2520,6 +2526,7 @@ const setupTranslations: Record<Locale, Record<string, string>> = {
"wizard.restarting": "再起動中...",
"wizard.shutdownConfirm": "デバイスをシャットダウンしますか?",
"wizard.restartConfirm": "デバイスを再起動しますか?",
"wizard.completionUnreachable": "デバイスに接続できませんでした。接続を確認してもう一度お試しください。",

// === ProgressBar ===
"progress.wifi": "WiFi",
Expand Down Expand Up @@ -2901,6 +2908,7 @@ const setupTranslations: Record<Locale, Record<string, string>> = {
"wizard.restarting": "Herstarten...",
"wizard.shutdownConfirm": "Apparaat uitschakelen?",
"wizard.restartConfirm": "Apparaat herstarten?",
"wizard.completionUnreachable": "Kan het apparaat niet bereiken. Controleer uw verbinding en probeer opnieuw.",

// === ProgressBar ===
"progress.wifi": "WiFi",
Expand Down Expand Up @@ -3282,6 +3290,7 @@ const setupTranslations: Record<Locale, Record<string, string>> = {
"wizard.restarting": "Startar om...",
"wizard.shutdownConfirm": "Stänga av enheten?",
"wizard.restartConfirm": "Starta om enheten?",
"wizard.completionUnreachable": "Det gick inte att nå enheten. Kontrollera anslutningen och försök igen.",

// === ProgressBar ===
"progress.wifi": "WiFi",
Expand Down Expand Up @@ -3663,6 +3672,7 @@ const setupTranslations: Record<Locale, Record<string, string>> = {
"wizard.restarting": "正在重启...",
"wizard.shutdownConfirm": "确定关机吗?",
"wizard.restartConfirm": "确定重启吗?",
"wizard.completionUnreachable": "无法连接到设备。请检查您的连接并重试。",

// === ProgressBar ===
"progress.wifi": "WiFi",
Expand Down
Loading