()
| 810 | let pollTimer: ReturnType<typeof setTimeout> | null = null; |
| 811 | |
| 812 | async function poll(): Promise<void> { |
| 813 | if (cancelled) return; |
| 814 | try { |
| 815 | const r = await getProvisioningProgressAction(slug); |
| 816 | if (cancelled) return; |
| 817 | if (!r.ok) { |
| 818 | setError(r.error); |
| 819 | return; |
| 820 | } |
| 821 | setSteps(r.steps); |
| 822 | if (r.overall === "failed") { |
| 823 | const failed = r.steps.find((s) => s.status === "failed"); |
| 824 | setError(failed?.error ?? "Provisioning failed."); |
| 825 | return; |
| 826 | } |
| 827 | if (r.overall === "done") { |
| 828 | // Resolve the CMO + first task slugs and forward to the live |
| 829 | // task workspace. Guarded so React StrictMode's double-mount |
| 830 | // doesn't fire two redirects. |
| 831 | if (redirectedRef.current) return; |
| 832 | redirectedRef.current = true; |
| 833 | const dest = await getOnboardingTaskForSkipAction(slug); |
| 834 | if (cancelled) return; |
| 835 | if (!dest.ok) { |
| 836 | setError(dest.error); |
| 837 | redirectedRef.current = false; |
| 838 | return; |
| 839 | } |
| 840 | router.replace( |
| 841 | projectHref( |
| 842 | slug, |
| 843 | `/agents/${dest.cmo_agent_slug}/tasks?task=${encodeURIComponent(dest.task_display_id)}`, |
| 844 | ), |
| 845 | ); |
| 846 | return; |
| 847 | } |
| 848 | // Still running — poll again. 500ms keeps the rows feeling |
| 849 | // alive without hammering the server. |
| 850 | pollTimer = setTimeout(poll, 500); |
| 851 | } catch (err) { |
| 852 | if (cancelled) return; |
| 853 | setError(err instanceof Error ? err.message : String(err)); |
| 854 | } |
| 855 | } |
| 856 | |
| 857 | void poll(); |
| 858 | return () => { |
no test coverage detected