Skip to content

Commit 734d491

Browse files
ralyodioclaude
andcommitted
Add "Retry all failed" button to /recent outreach
Retries every failed/timed-out row in one click instead of clicking each Retry. Runs the existing retryRecentOutreach sequentially (the browser concurrency cap already throttles the worker) with a live "Retrying x/N…" counter, then refreshes so the live status/progress bar takes over. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 84c3fc5 commit 734d491

1 file changed

Lines changed: 52 additions & 8 deletions

File tree

‎app/(marketing)/recent/outreach-form.tsx‎

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -131,14 +131,21 @@ export function RecentOutreachForm({
131131
<div className="mt-3 space-y-1.5 border-b border-[var(--color-border)] pb-3 text-xs">
132132
<div className="flex items-center justify-between gap-2">
133133
<span className="font-medium text-[var(--color-muted)]">History</span>
134-
{progress.inFlight ? (
135-
<span className="inline-flex items-center gap-1.5 text-[var(--color-muted)]">
136-
<span className="inline-block h-2 w-2 animate-pulse rounded-full bg-blue-500" />
137-
Live · updating…
138-
</span>
139-
) : (
140-
<span className="text-[var(--color-muted)]">Idle</span>
141-
)}
134+
<span className="inline-flex items-center gap-2">
135+
<RetryAllFailedButton
136+
messageIds={history
137+
.filter((h) => h.status === "failed" || h.status === "timed_out")
138+
.map((h) => h.id)}
139+
/>
140+
{progress.inFlight ? (
141+
<span className="inline-flex items-center gap-1.5 text-[var(--color-muted)]">
142+
<span className="inline-block h-2 w-2 animate-pulse rounded-full bg-blue-500" />
143+
Live · updating…
144+
</span>
145+
) : (
146+
<span className="text-[var(--color-muted)]">Idle</span>
147+
)}
148+
</span>
142149
</div>
143150
<OutreachProgressBar progress={progress} />
144151
{history.map((h) => (
@@ -322,6 +329,43 @@ export function RecentOutreachForm({
322329
);
323330
}
324331

332+
function RetryAllFailedButton({ messageIds }: { messageIds: string[] }) {
333+
const router = useRouter();
334+
const [pending, start] = useTransition();
335+
const [progress, setProgress] = useState<{ done: number; total: number } | null>(null);
336+
337+
if (messageIds.length === 0) return null;
338+
339+
return (
340+
<span className="inline-flex items-center gap-1.5">
341+
<button
342+
type="button"
343+
disabled={pending}
344+
onClick={() => {
345+
start(async () => {
346+
let done = 0;
347+
setProgress({ done, total: messageIds.length });
348+
// Sequential on purpose: retries fan out to the worker, and the
349+
// browser-concurrency cap already limits it — no need to stampede.
350+
for (const id of messageIds) {
351+
await retryRecentOutreach({ messageId: id });
352+
done += 1;
353+
setProgress({ done, total: messageIds.length });
354+
}
355+
setProgress(null);
356+
router.refresh();
357+
});
358+
}}
359+
className="rounded border border-[var(--color-border)] px-2 py-0.5 font-medium hover:bg-[var(--color-card)] disabled:opacity-50"
360+
>
361+
{pending && progress
362+
? `Retrying ${progress.done}/${progress.total}…`
363+
: `Retry all failed (${messageIds.length})`}
364+
</button>
365+
</span>
366+
);
367+
}
368+
325369
function RetryOutreachButton({ messageId }: { messageId: string }) {
326370
const router = useRouter();
327371
const [pending, start] = useTransition();

0 commit comments

Comments
 (0)