diff options
Diffstat (limited to 'makima/frontend/src/routes')
| -rw-r--r-- | makima/frontend/src/routes/daemons.tsx | 261 |
1 files changed, 95 insertions, 166 deletions
diff --git a/makima/frontend/src/routes/daemons.tsx b/makima/frontend/src/routes/daemons.tsx index ca167fe..aa48deb 100644 --- a/makima/frontend/src/routes/daemons.tsx +++ b/makima/frontend/src/routes/daemons.tsx @@ -6,7 +6,6 @@ import { listDaemons, restartDaemon, triggerDaemonReauth, - submitDaemonAuthCode, getDaemonReauthStatus, type Daemon, type DaemonListResponse, @@ -43,7 +42,7 @@ function ErrorAlert({ children }: { children: React.ReactNode }) { type ReauthState = | { phase: "initiating" } | { phase: "url_ready"; loginUrl: string; requestId: string } - | { phase: "submitting"; requestId: string } + | { phase: "waiting_for_auth"; loginUrl: string; requestId: string } | { phase: "success" } | { phase: "error"; message: string }; @@ -55,7 +54,6 @@ function ReauthModal({ onClose: () => void; }) { const [state, setState] = useState<ReauthState>({ phase: "initiating" }); - const [authCode, setAuthCode] = useState(""); const pollingRef = useRef<ReturnType<typeof setInterval> | null>(null); // Cleanup polling on unmount @@ -67,6 +65,53 @@ function ReauthModal({ }; }, []); + // Start polling for status updates (URL ready, then completion) + const startPolling = useCallback( + (requestId: string) => { + if (pollingRef.current) { + clearInterval(pollingRef.current); + } + pollingRef.current = setInterval(async () => { + try { + const status = await getDaemonReauthStatus(daemon.id, requestId); + + if (status.status === "completed") { + setState({ phase: "success" }); + if (pollingRef.current) { + clearInterval(pollingRef.current); + pollingRef.current = null; + } + } else if (status.status === "url_ready" && status.loginUrl) { + setState((prev) => { + // Only update if we haven't already shown the URL + if (prev.phase === "initiating") { + return { + phase: "url_ready", + loginUrl: status.loginUrl!, + requestId, + }; + } + return prev; + }); + // Keep polling for completion - don't stop here + } else if (status.status === "failed") { + setState({ + phase: "error", + message: status.error || "Reauth failed", + }); + if (pollingRef.current) { + clearInterval(pollingRef.current); + pollingRef.current = null; + } + } + } catch { + // Polling errors are non-fatal, keep trying + } + }, 2000); + }, + [daemon.id], + ); + // Trigger reauth on mount useEffect(() => { let cancelled = false; @@ -74,45 +119,7 @@ function ReauthModal({ try { const res = await triggerDaemonReauth(daemon.id); if (cancelled) return; - - // Start polling for status - const requestId = res.requestId; - pollingRef.current = setInterval(async () => { - try { - const status = await getDaemonReauthStatus(daemon.id, requestId); - if (cancelled) return; - - if (status.status === "url_ready" && status.loginUrl) { - setState({ - phase: "url_ready", - loginUrl: status.loginUrl, - requestId, - }); - // Stop polling once we have the URL - if (pollingRef.current) { - clearInterval(pollingRef.current); - pollingRef.current = null; - } - } else if (status.status === "failed") { - setState({ - phase: "error", - message: status.error || "Reauth failed", - }); - if (pollingRef.current) { - clearInterval(pollingRef.current); - pollingRef.current = null; - } - } else if (status.status === "completed") { - setState({ phase: "success" }); - if (pollingRef.current) { - clearInterval(pollingRef.current); - pollingRef.current = null; - } - } - } catch { - // Polling errors are non-fatal, keep trying - } - }, 2000); + startPolling(res.requestId); } catch (err) { if (cancelled) return; setState({ @@ -126,110 +133,28 @@ function ReauthModal({ return () => { cancelled = true; }; - }, [daemon.id]); - - const handleSubmitCode = useCallback( - async (e: React.FormEvent) => { - e.preventDefault(); - if (!authCode.trim() || state.phase !== "url_ready") return; - - const requestId = state.requestId; - setState({ phase: "submitting", requestId }); - - try { - await submitDaemonAuthCode(daemon.id, authCode.trim(), requestId); - - // Poll for completion - pollingRef.current = setInterval(async () => { - try { - const status = await getDaemonReauthStatus( - daemon.id, - requestId, - ); - if (status.status === "completed") { - setState({ phase: "success" }); - if (pollingRef.current) { - clearInterval(pollingRef.current); - pollingRef.current = null; - } - } else if (status.status === "failed") { - setState({ - phase: "error", - message: status.error || "Auth code submission failed", - }); - if (pollingRef.current) { - clearInterval(pollingRef.current); - pollingRef.current = null; - } - } - } catch { - // Keep polling - } - }, 2000); - - // Also set a timeout so we don't poll forever - setTimeout(() => { - if (pollingRef.current) { - clearInterval(pollingRef.current); - pollingRef.current = null; - } - // If still submitting after 30s, assume success (setup-token completed) - setState((prev) => - prev.phase === "submitting" ? { phase: "success" } : prev, - ); - }, 30000); - } catch (err) { - setState({ - phase: "error", - message: - err instanceof Error - ? err.message - : "Failed to submit auth code", - }); + }, [daemon.id, startPolling]); + + // When URL is shown, transition to waiting_for_auth after user clicks the link + const handleOpenedLink = useCallback(() => { + setState((prev) => { + if (prev.phase === "url_ready") { + return { + phase: "waiting_for_auth", + loginUrl: prev.loginUrl, + requestId: prev.requestId, + }; } - }, - [authCode, daemon.id, state], - ); + return prev; + }); + }, []); const handleRetry = useCallback(() => { - setAuthCode(""); setState({ phase: "initiating" }); - // Re-trigger will happen via the useEffect dependency change - // We need to manually trigger since daemon.id hasn't changed const trigger = async () => { try { const res = await triggerDaemonReauth(daemon.id); - const requestId = res.requestId; - pollingRef.current = setInterval(async () => { - try { - const status = await getDaemonReauthStatus( - daemon.id, - requestId, - ); - if (status.status === "url_ready" && status.loginUrl) { - setState({ - phase: "url_ready", - loginUrl: status.loginUrl, - requestId, - }); - if (pollingRef.current) { - clearInterval(pollingRef.current); - pollingRef.current = null; - } - } else if (status.status === "failed") { - setState({ - phase: "error", - message: status.error || "Reauth failed", - }); - if (pollingRef.current) { - clearInterval(pollingRef.current); - pollingRef.current = null; - } - } - } catch { - // Keep polling - } - }, 2000); + startPolling(res.requestId); } catch (err) { setState({ phase: "error", @@ -241,7 +166,7 @@ function ReauthModal({ } }; trigger(); - }, [daemon.id]); + }, [daemon.id, startPolling]); return ( <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60"> @@ -272,46 +197,50 @@ function ReauthModal({ </div> )} - {/* URL Ready */} + {/* URL Ready - user needs to click the link */} {state.phase === "url_ready" && ( <div className="space-y-3"> <p className="text-[10px] font-mono text-[#7788aa]"> - Click the button below to open the OAuth login page, then paste the code: + Click the button below to open the OAuth login page. Authentication will complete automatically. </p> <a href={state.loginUrl} target="_blank" rel="noopener noreferrer" + onClick={handleOpenedLink} className="block text-center bg-amber-500 hover:bg-amber-400 text-black font-mono text-xs font-medium px-4 py-2 transition-colors" > - 1. Login to Claude + Login to Claude </a> - <form onSubmit={handleSubmitCode} className="flex gap-2"> - <input - type="text" - value={authCode} - onChange={(e) => setAuthCode(e.target.value)} - placeholder="2. Paste authentication code" - className="flex-1 bg-[#0a1525] border border-amber-500/30 px-3 py-2 text-xs font-mono text-amber-100 placeholder-amber-500/50 focus:outline-none focus:border-amber-400" - /> - <button - type="submit" - disabled={!authCode.trim()} - className="bg-amber-500 hover:bg-amber-400 disabled:bg-amber-700 disabled:cursor-not-allowed text-black font-mono text-xs font-medium px-4 py-2 transition-colors" - > - Submit - </button> - </form> + <div className="flex items-center gap-2 pt-1"> + <div className="w-2 h-2 bg-amber-500/50 rounded-full animate-pulse" /> + <span className="text-[10px] font-mono text-[#7788aa]"> + Waiting for authentication to complete... + </span> + </div> </div> )} - {/* Submitting */} - {state.phase === "submitting" && ( - <div className="flex items-center gap-2 py-4"> - <div className="w-3 h-3 border border-amber-400 border-t-transparent rounded-full animate-spin" /> - <span className="text-[10px] font-mono text-[#7788aa]"> - Submitting auth code... - </span> + {/* Waiting for auth - user has clicked the link, waiting for token */} + {state.phase === "waiting_for_auth" && ( + <div className="space-y-3"> + <div className="flex items-center gap-2 py-2"> + <div className="w-3 h-3 border border-amber-400 border-t-transparent rounded-full animate-spin" /> + <span className="text-[10px] font-mono text-[#7788aa]"> + Waiting for authentication to complete... + </span> + </div> + <p className="text-[10px] font-mono text-[#556677]"> + Complete the login in your browser. The token will be saved automatically. + </p> + <a + href={state.loginUrl} + target="_blank" + rel="noopener noreferrer" + className="inline-block text-[10px] font-mono text-amber-500/70 hover:text-amber-400 underline" + > + Open login page again + </a> </div> )} |
