From feb35ff38450878889502f8bcfd4d2d2dcb777e9 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Sat, 4 Jul 2026 02:44:03 +0000 Subject: [PATCH] Live status + progress bar for /recent outreach history MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The outreach history on /recent was static — browser/cookie posts move asynchronously (queued → awaiting_code → sent/failed as the worker churns through them), but you had to reload to see any change. Now the form polls router.refresh() every 8s while anything is still in flight and stops once everything is terminal, so rows update in real time. Adds a segmented progress bar (sent / failed / pending) with an "X/Y done" count and a "Live · updating…" pulse while posts are in flight. Co-Authored-By: Claude Opus 4.8 --- app/(marketing)/recent/outreach-form.tsx | 74 +++++++++++++++++++++++- 1 file changed, 72 insertions(+), 2 deletions(-) diff --git a/app/(marketing)/recent/outreach-form.tsx b/app/(marketing)/recent/outreach-form.tsx index 8565e572..6b2b0f33 100644 --- a/app/(marketing)/recent/outreach-form.tsx +++ b/app/(marketing)/recent/outreach-form.tsx @@ -2,7 +2,7 @@ import Link from "next/link"; import { useRouter } from "next/navigation"; -import { FormEvent, useMemo, useState, useTransition } from "react"; +import { FormEvent, useEffect, useMemo, useState, useTransition } from "react"; import { retryRecentOutreach, sendRecentAuditOutreach, @@ -42,6 +42,7 @@ export function RecentOutreachForm({ creditsBalance: number; history: OutreachHistoryItem[]; }) { + const router = useRouter(); const initialChannel = hasEmail ? "email" : hasPhone ? "sms" : "social"; const [channel, setChannel] = useState<"email" | "sms" | "social">(initialChannel); const [visibility, setVisibility] = useState<"private" | "public">("private"); @@ -67,6 +68,19 @@ export function RecentOutreachForm({ ); const insufficient = cost > 0 && creditsBalance < cost; + // Progress across this audit's outreach history. Cookie/browser posts move + // asynchronously (queued → awaiting_code/publishing → sent/failed) as the + // worker churns through them, so summarize where they stand. + const progress = useMemo(() => summarizeHistory(history), [history]); + + // Poll while anything is still in flight so rows update in real time without + // a manual reload; stop once everything is terminal to avoid idle load. + useEffect(() => { + if (!progress.inFlight) return; + const id = setInterval(() => router.refresh(), OUTREACH_REFRESH_MS); + return () => clearInterval(id); + }, [progress.inFlight, router]); + function submit(event: FormEvent) { event.preventDefault(); setMessage(null); @@ -115,7 +129,18 @@ export function RecentOutreachForm({ {history.length > 0 && (
-
History
+
+ History + {progress.inFlight ? ( + + + Live · updating… + + ) : ( + Idle + )} +
+ {history.map((h) => (
{statusLabel(h.status)} @@ -331,6 +356,51 @@ function RetryOutreachButton({ messageId }: { messageId: string }) { ); } +const OUTREACH_REFRESH_MS = 8000; + +type OutreachProgress = { + total: number; + sent: number; + failed: number; + pending: number; + inFlight: boolean; +}; + +// A post is "pending" (still moving) while queued or awaiting a code; "failed" +// covers failed + timed out. inFlight drives the live-refresh loop. +function summarizeHistory(history: OutreachHistoryItem[]): OutreachProgress { + let sent = 0; + let failed = 0; + let pending = 0; + for (const h of history) { + if (h.status === "sent") sent++; + else if (h.status === "failed" || h.status === "timed_out") failed++; + else pending++; // queued | awaiting_code + } + return { total: history.length, sent, failed, pending, inFlight: pending > 0 }; +} + +function OutreachProgressBar({ progress }: { progress: OutreachProgress }) { + const { total, sent, failed, pending } = progress; + if (total === 0) return null; + const pct = (n: number) => `${(n / total) * 100}%`; + return ( +
+
+
+
+
+
+
+ {sent} sent · {failed} failed · {pending} pending + + ({sent + failed}/{total} done) + +
+
+ ); +} + function defaultBody(host: string) { return `I saw your CrawlProof audit for ${host}. There are a few concrete fixes that would improve how AI engines understand the site.`; }