-
Notifications
You must be signed in to change notification settings - Fork 0
agent: @U0AJM7X8FBR Now that Admin shows me Pulse Runs on /accounts/[account_id #11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
0f8be89
534c4ff
2253f15
0aa8ef0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| "use client"; | ||
|
|
||
| import { usePulseEmail } from "@/hooks/usePulseEmail"; | ||
| import { getEmailIdFromTags } from "@/lib/tasks/getEmailIdFromTags"; | ||
| import type { TaskRun } from "@/types/sandbox"; | ||
| import PulseEmailModalHeader from "./PulseEmailModalHeader"; | ||
| import PulseEmailModalBody from "./PulseEmailModalBody"; | ||
|
|
||
| interface PulseEmailModalProps { | ||
| run: TaskRun; | ||
| onClose: () => void; | ||
| } | ||
|
|
||
| export default function PulseEmailModal({ run, onClose }: PulseEmailModalProps) { | ||
| const emailId = getEmailIdFromTags(run.tags); | ||
| const { data: email, isLoading, error } = usePulseEmail(emailId); | ||
|
|
||
| return ( | ||
| <div | ||
| className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4" | ||
| onClick={onClose} | ||
| > | ||
| <div | ||
| className="relative flex flex-col w-full max-w-3xl max-h-[90vh] rounded-xl bg-white dark:bg-gray-900 shadow-2xl overflow-hidden" | ||
| onClick={(e) => e.stopPropagation()} | ||
| > | ||
| <PulseEmailModalHeader | ||
| email={email ?? null} | ||
| runId={run.id} | ||
| onClose={onClose} | ||
| /> | ||
|
|
||
| <div className="flex-1 overflow-auto"> | ||
| <PulseEmailModalBody | ||
| email={email ?? null} | ||
| isLoading={isLoading} | ||
| error={error as Error | null} | ||
| /> | ||
| </div> | ||
|
|
||
| {email && ( | ||
| <div className="px-6 py-3 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800"> | ||
| <p className="text-xs text-gray-400 font-mono">Resend ID: {email.id}</p> | ||
| </div> | ||
| )} | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,51 @@ | ||||||||||||||||||||||
| import type { PulseEmail } from "@/lib/recoup/fetchAccountPulseEmails"; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| interface PulseEmailModalBodyProps { | ||||||||||||||||||||||
| email: PulseEmail | null; | ||||||||||||||||||||||
| isLoading: boolean; | ||||||||||||||||||||||
| error: Error | null; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| export default function PulseEmailModalBody({ email, isLoading, error }: PulseEmailModalBodyProps) { | ||||||||||||||||||||||
| if (isLoading) { | ||||||||||||||||||||||
| return ( | ||||||||||||||||||||||
| <div className="flex items-center justify-center py-20 text-sm text-gray-500 dark:text-gray-400"> | ||||||||||||||||||||||
| Loading email… | ||||||||||||||||||||||
| </div> | ||||||||||||||||||||||
| ); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| if (error) { | ||||||||||||||||||||||
| return ( | ||||||||||||||||||||||
| <div className="flex items-center justify-center py-20 text-sm text-red-500"> | ||||||||||||||||||||||
| Failed to load email: {error.message} | ||||||||||||||||||||||
| </div> | ||||||||||||||||||||||
| ); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| if (!email) { | ||||||||||||||||||||||
| return ( | ||||||||||||||||||||||
| <div className="flex items-center justify-center py-20 text-sm text-gray-500 dark:text-gray-400"> | ||||||||||||||||||||||
| No email found for this run. | ||||||||||||||||||||||
| </div> | ||||||||||||||||||||||
| ); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| if (email.html) { | ||||||||||||||||||||||
| return ( | ||||||||||||||||||||||
| <iframe | ||||||||||||||||||||||
| srcDoc={email.html} | ||||||||||||||||||||||
| title="Email preview" | ||||||||||||||||||||||
| sandbox="allow-same-origin" | ||||||||||||||||||||||
| className="w-full border-0" | ||||||||||||||||||||||
|
Comment on lines
+36
to
+40
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Tighten iframe sandbox for untrusted email HTML. At Line 39, Proposed fix <iframe
srcDoc={email.html}
title="Email preview"
- sandbox="allow-same-origin"
+ sandbox=""
className="w-full border-0"
style={{ minHeight: "500px" }}
/>📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||
| style={{ minHeight: "500px" }} | ||||||||||||||||||||||
| /> | ||||||||||||||||||||||
| ); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| return ( | ||||||||||||||||||||||
| <div className="px-6 py-8 text-sm text-gray-600 dark:text-gray-300 whitespace-pre-wrap"> | ||||||||||||||||||||||
| (No HTML content — email may have been text-only) | ||||||||||||||||||||||
| </div> | ||||||||||||||||||||||
| ); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| import type { PulseEmail } from "@/lib/recoup/fetchAccountPulseEmails"; | ||
|
|
||
| interface PulseEmailModalHeaderProps { | ||
| email: PulseEmail | null; | ||
| runId: string; | ||
| onClose: () => void; | ||
| } | ||
|
|
||
| export default function PulseEmailModalHeader({ email, runId, onClose }: PulseEmailModalHeaderProps) { | ||
| return ( | ||
| <div className="flex items-start justify-between gap-4 px-6 py-4 border-b border-gray-200 dark:border-gray-700"> | ||
| <div className="min-w-0"> | ||
| <p className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1"> | ||
| Pulse Email Preview | ||
| </p> | ||
| {email ? ( | ||
| <> | ||
| <h2 className="text-base font-semibold text-gray-900 dark:text-gray-100 truncate"> | ||
| {email.subject ?? "(no subject)"} | ||
| </h2> | ||
| <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5"> | ||
| To: {email.to.join(", ")} ·{" "} | ||
| {new Date(email.created_at).toLocaleString()} | ||
| </p> | ||
| </> | ||
| ) : ( | ||
| <p className="text-sm text-gray-500 dark:text-gray-400 font-mono">{runId}</p> | ||
| )} | ||
| </div> | ||
| <button | ||
| onClick={onClose} | ||
| className="shrink-0 rounded-lg p-1.5 text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors" | ||
| aria-label="Close" | ||
| > | ||
| <svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | ||
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /> | ||
| </svg> | ||
| </button> | ||
| </div> | ||
| ); | ||
| } |
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. are both hooks/usePulseEmail.ts and hooks/usePulseEmails.ts being used? |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| "use client"; | ||
|
|
||
| import { useQuery } from "@tanstack/react-query"; | ||
| import { usePrivy } from "@privy-io/react-auth"; | ||
| import { fetchPulseEmailById } from "@/lib/recoup/fetchPulseEmailById"; | ||
|
|
||
| /** | ||
| * Fetches a single Resend email by ID. | ||
| * Query key includes emailId so switching runs invalidates stale data. | ||
| */ | ||
| export function usePulseEmail(emailId: string | null) { | ||
| const { ready, authenticated, getAccessToken } = usePrivy(); | ||
|
|
||
| return useQuery({ | ||
| queryKey: ["admin", "pulse-email", emailId], | ||
| queryFn: async () => { | ||
| const token = await getAccessToken(); | ||
| if (!token) throw new Error("Not authenticated"); | ||
| return fetchPulseEmailById(token, emailId!); | ||
| }, | ||
| enabled: ready && authenticated && !!emailId, | ||
| }); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| import { API_BASE_URL } from "@/lib/consts"; | ||
|
|
||
| /** | ||
| * Full Resend GetEmailResponseSuccess shape. | ||
| * See https://resend.com/docs/api-reference/emails/retrieve-email | ||
| */ | ||
| export interface PulseEmail { | ||
| id: string; | ||
| from: string; | ||
| to: string[]; | ||
| cc: string[] | null; | ||
| bcc: string[] | null; | ||
| reply_to: string[] | null; | ||
| subject: string; | ||
| html: string | null; | ||
| text: string | null; | ||
| created_at: string; | ||
| scheduled_at: string | null; | ||
| last_event: string; | ||
| tags?: { name: string; value: string }[]; | ||
| } | ||
|
sweetmantech marked this conversation as resolved.
|
||
|
|
||
| /** | ||
| * Fetches all Resend emails sent for a specific account from GET /api/admins/emails?account_id=<id>. | ||
| * Authenticates using the caller's Privy access token (admin Bearer auth). | ||
| * | ||
| * @param accessToken - Privy access token from getAccessToken() | ||
| * @param accountId - The account ID to fetch emails for | ||
| * @returns Array of pulse emails with HTML content | ||
| */ | ||
| export async function fetchAccountPulseEmails( | ||
| accessToken: string, | ||
| accountId: string, | ||
| ): Promise<PulseEmail[]> { | ||
| const url = new URL(`${API_BASE_URL}/api/admins/emails`); | ||
| url.searchParams.set("account_id", accountId); | ||
|
|
||
| const res = await fetch(url.toString(), { | ||
| headers: { Authorization: `Bearer ${accessToken}` }, | ||
| }); | ||
|
|
||
| if (!res.ok) { | ||
| const body = await res.json().catch(() => ({})); | ||
| throw new Error(body.error ?? body.message ?? `HTTP ${res.status}`); | ||
| } | ||
|
|
||
| const data = await res.json(); | ||
| return data.emails ?? []; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| import { API_BASE_URL } from "@/lib/consts"; | ||
| import type { PulseEmail } from "./fetchAccountPulseEmails"; | ||
|
|
||
| /** | ||
| * Fetches a single Resend email by ID from GET /api/admins/emails?email_id=<id>. | ||
| * | ||
| * @param accessToken - Privy access token | ||
| * @param emailId - The Resend email ID | ||
| * @returns The email object or null if not found | ||
| */ | ||
| export async function fetchPulseEmailById( | ||
| accessToken: string, | ||
| emailId: string, | ||
| ): Promise<PulseEmail | null> { | ||
| const url = new URL(`${API_BASE_URL}/api/admins/emails`); | ||
| url.searchParams.set("email_id", emailId); | ||
|
|
||
| const res = await fetch(url.toString(), { | ||
| headers: { Authorization: `Bearer ${accessToken}` }, | ||
| }); | ||
|
|
||
| if (!res.ok) { | ||
| const body = await res.json().catch(() => ({})); | ||
| throw new Error(body.error ?? body.message ?? `HTTP ${res.status}`); | ||
| } | ||
|
|
||
| const data = await res.json(); | ||
| return data.emails?.[0] ?? null; | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,11 @@ | ||||||||||||||||||
| /** | ||||||||||||||||||
| * Extracts the Resend email ID from a task run's tags. | ||||||||||||||||||
| * Looks for tags matching the pattern "email:<emailId>". | ||||||||||||||||||
| * | ||||||||||||||||||
| * @param tags - Array of tag strings from the task run | ||||||||||||||||||
| * @returns The Resend email ID or null if not found | ||||||||||||||||||
| */ | ||||||||||||||||||
| export function getEmailIdFromTags(tags: string[]): string | null { | ||||||||||||||||||
| const emailTag = tags.find((t) => t.startsWith("email:")); | ||||||||||||||||||
| return emailTag ? emailTag.slice("email:".length) : null; | ||||||||||||||||||
|
Comment on lines
+9
to
+10
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Normalize extracted IDs so empty tags don’t leak through. At Line 10, a tag like Proposed fix export function getEmailIdFromTags(tags: string[]): string | null {
const emailTag = tags.find((t) => t.startsWith("email:"));
- return emailTag ? emailTag.slice("email:".length) : null;
+ if (!emailTag) return null;
+ const id = emailTag.slice("email:".length).trim();
+ return id.length > 0 ? id : null;
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||
| } | ||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add modal a11y semantics and Escape-key dismissal.
At Lines 19-47, the overlay behaves like a dialog but lacks
role="dialog",aria-modal, and keyboard Escape handling, which can block keyboard/screen-reader workflows.Proposed fix
🤖 Prompt for AI Agents