From 0f8be893d2a03fddee3f7d84813b225ef1b0b209 Mon Sep 17 00:00:00 2001 From: Recoup Agent Date: Mon, 16 Mar 2026 22:09:56 +0000 Subject: [PATCH 1/4] feat: pulse run onClick shows Resend email HTML in a modal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - TaskRunsTable: add optional onRunClick prop; rows are cursor-pointer when clickable - AccountDetailPage: track selectedRun state, pass onRunClick to pulse runs table - PulseEmailModal: fetches all emails for the account, matches the email closest to the task run's time window (±5 min), renders HTML in a sandboxed iframe - usePulseEmails: react-query hook (lazy, enabled only when modal opens) - fetchAccountPulseEmails: calls GET /api/admins/emails?account_id= Co-Authored-By: Claude Sonnet 4.6 --- .../AccountDetail/AccountDetailPage.tsx | 19 ++- components/AccountDetail/PulseEmailModal.tsx | 129 ++++++++++++++++++ components/AccountDetail/TaskRunsTable.tsx | 9 +- hooks/usePulseEmails.ts | 23 ++++ lib/recoup/fetchAccountPulseEmails.ts | 38 ++++++ 5 files changed, 215 insertions(+), 3 deletions(-) create mode 100644 components/AccountDetail/PulseEmailModal.tsx create mode 100644 hooks/usePulseEmails.ts create mode 100644 lib/recoup/fetchAccountPulseEmails.ts diff --git a/components/AccountDetail/AccountDetailPage.tsx b/components/AccountDetail/AccountDetailPage.tsx index 895a253..f09620e 100644 --- a/components/AccountDetail/AccountDetailPage.tsx +++ b/components/AccountDetail/AccountDetailPage.tsx @@ -1,9 +1,12 @@ "use client"; +import { useState } from "react"; import { useAccountTaskRuns } from "@/hooks/useAccountTaskRuns"; import AccountBreadcrumb from "./AccountBreadcrumb"; import TaskRunsTable from "./TaskRunsTable"; +import PulseEmailModal from "./PulseEmailModal"; import TableSkeleton from "@/components/Sandboxes/TableSkeleton"; +import type { TaskRun } from "@/types/sandbox"; const TASK_RUN_COLUMNS = ["Task", "Status", "Started", "Duration", "Run ID"]; @@ -13,6 +16,7 @@ interface AccountDetailPageProps { export default function AccountDetailPage({ accountId }: AccountDetailPageProps) { const { data: runs, isLoading, error } = useAccountTaskRuns(accountId); + const [selectedRun, setSelectedRun] = useState(null); const pulseRuns = runs?.filter(r => r.taskIdentifier === "send-pulse-task") ?? []; @@ -52,7 +56,12 @@ export default function AccountDetailPage({ accountId }: AccountDetailPageProps) )} - {!isLoading && !error && } + {!isLoading && !error && ( + setSelectedRun(run)} + /> + )} {!isLoading && !error && runs && runs.length > pulseRuns.length && (
@@ -65,6 +74,14 @@ export default function AccountDetailPage({ accountId }: AccountDetailPageProps)
)} + + {selectedRun && ( + setSelectedRun(null)} + /> + )} ); } diff --git a/components/AccountDetail/PulseEmailModal.tsx b/components/AccountDetail/PulseEmailModal.tsx new file mode 100644 index 0000000..ac2e9bb --- /dev/null +++ b/components/AccountDetail/PulseEmailModal.tsx @@ -0,0 +1,129 @@ +"use client"; + +import { usePulseEmails } from "@/hooks/usePulseEmails"; +import type { TaskRun } from "@/types/sandbox"; +import type { PulseEmail } from "@/lib/recoup/fetchAccountPulseEmails"; + +interface PulseEmailModalProps { + accountId: string; + run: TaskRun; + onClose: () => void; +} + +/** + * Finds the email most likely sent during a given task run by matching + * the email's created_at against the run's startedAt/finishedAt window. + * Falls back to the most recent email if no match is found. + */ +function findEmailForRun(emails: PulseEmail[], run: TaskRun): PulseEmail | null { + if (emails.length === 0) return null; + + const start = run.startedAt ? new Date(run.startedAt).getTime() : null; + const end = run.finishedAt ? new Date(run.finishedAt).getTime() : null; + + if (start && end) { + // Add a 5-minute buffer on each side to account for clock skew + const buffer = 5 * 60 * 1000; + const match = emails.find((e) => { + const t = new Date(e.created_at).getTime(); + return t >= start - buffer && t <= end + buffer; + }); + if (match) return match; + } + + // Fallback: most recent email + return emails[0]; +} + +export default function PulseEmailModal({ accountId, run, onClose }: PulseEmailModalProps) { + const { data: emails, isLoading, error } = usePulseEmails(accountId, true); + + const email = emails ? findEmailForRun(emails, run) : null; + + return ( +
+
e.stopPropagation()} + > + {/* Header */} +
+
+

+ Pulse Email Preview +

+ {email ? ( + <> +

+ {email.subject ?? "(no subject)"} +

+

+ To: {email.to.join(", ")} ·{" "} + {new Date(email.created_at).toLocaleString()} +

+ + ) : ( +

{run.id}

+ )} +
+ +
+ + {/* Body */} +
+ {isLoading && ( +
+ Loading email… +
+ )} + + {error && ( +
+ Failed to load email: {error instanceof Error ? error.message : "Unknown error"} +
+ )} + + {!isLoading && !error && !email && ( +
+ No email found for this run. +
+ )} + + {!isLoading && !error && email?.html && ( +