@agent-native/dispatch 0.34.0 → 0.35.0-nightly-20260908153602

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"transactional-emails.d.ts","sourceRoot":"","sources":["../../src/client/transactional-emails.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,WAAW,qBAAqB;IACpC,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,6EAA6E;IAC7E,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,qBAAqB,EAAE,CAAC;IAChC,4DAA4D;IAC5D,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,+DAA+D;IAC/D,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,MAAM,WAAW,8BAA8B;IAC7C,GAAG,EAAE,MAAM,CAAC;IACZ,cAAc,EAAE,OAAO,CAAC;IACxB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,MAAM,EAAE,qBAAqB,EAAE,CAAC;CACjC;AAED,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,8BAA8B,EACrC,WAAW,EAAE,eAAe,EAAE,GAC7B;IAAE,MAAM,EAAE,qBAAqB,EAAE,CAAC;IAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CA4ChE;AAOD,wBAAsB,aAAa,CAAC,CAAC,EACnC,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,MAAM,EAAE,KAAK,GAAG,MAAM,GACrB,OAAO,CAAC,CAAC,CAAC,CAmBZ;AASD,wBAAsB,oBAAoB,CACxC,GAAG,EAAE;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EAC/C,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,eAAe,CAAC,CA0C1B;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,uEAAuE;AACvE,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,MAAM,EACf,EAAE,EAAE,MAAM,GACT,OAAO,CAAC,YAAY,CAAC,CAavB"}
1
+ {"version":3,"file":"transactional-emails.d.ts","sourceRoot":"","sources":["../../src/client/transactional-emails.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,WAAW,qBAAqB;IACpC,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,6EAA6E;IAC7E,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,qBAAqB,EAAE,CAAC;IAChC,4DAA4D;IAC5D,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,+DAA+D;IAC/D,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,MAAM,WAAW,8BAA8B;IAC7C,GAAG,EAAE,MAAM,CAAC;IACZ,cAAc,EAAE,OAAO,CAAC;IACxB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,MAAM,EAAE,qBAAqB,EAAE,CAAC;CACjC;AAED,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,8BAA8B,EACrC,WAAW,EAAE,eAAe,EAAE,GAC7B;IAAE,MAAM,EAAE,qBAAqB,EAAE,CAAC;IAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CA4ChE;AAOD,wBAAsB,aAAa,CAAC,CAAC,EACnC,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,MAAM,EAAE,KAAK,GAAG,MAAM,GACrB,OAAO,CAAC,CAAC,CAAC,CAuBZ;AASD,wBAAsB,oBAAoB,CACxC,GAAG,EAAE;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EAC/C,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,eAAe,CAAC,CA0C1B;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,uEAAuE;AACvE,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,MAAM,EACf,EAAE,EAAE,MAAM,GACT,OAAO,CAAC,YAAY,CAAC,CAavB"}
@@ -63,8 +63,11 @@ export async function callAppAction(appPath, action, params, method) {
63
63
  },
64
64
  ...(method === "POST" ? { body: JSON.stringify(params) } : {}),
65
65
  });
66
- if (!res.ok)
67
- throw new Error(`${action} failed: HTTP ${res.status}`);
66
+ if (!res.ok) {
67
+ const error = new Error(`${action} failed: HTTP ${res.status}`);
68
+ error.status = res.status;
69
+ throw error;
70
+ }
68
71
  return (await res.json());
69
72
  }
70
73
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"transactional-emails.js","sourceRoot":"","sources":["../../src/client/transactional-emails.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Reads each mounted app's transactional email catalog from the browser.\n *\n * Workspace apps are path-mounted on a single origin, so the browser can call\n * another app's action endpoint directly and its own session cookie carries the\n * caller's identity. Fanning out here rather than server-side keeps every app's\n * existing access checks in force — Dispatch never sees a catalog the signed-in\n * user could not have loaded themselves.\n */\n\nexport interface AppTransactionalEmail {\n id: string;\n app: string;\n name: string;\n trigger: string;\n recipient: string;\n recipientLabel: string;\n sender: string;\n senderLabel: string;\n /** null when the send log could not be read — not the same as zero sends. */\n sent: number | null;\n failed: number | null;\n lastSentAt: number | null;\n}\n\nexport interface AppEmailCatalog {\n appId: string;\n appName: string;\n appPath: string;\n emails: AppTransactionalEmail[];\n /** Set when this app's catalog could not be read at all. */\n error: string | null;\n /** Set when the catalog loaded but its send counts did not. */\n statsError: string | null;\n}\n\nexport interface LocalTransactionalEmailCatalog {\n app: string;\n statsAvailable: boolean;\n statsError: string | null;\n emails: AppTransactionalEmail[];\n}\n\nexport function aggregateSharedEmails(\n local: LocalTransactionalEmailCatalog,\n appCatalogs: AppEmailCatalog[],\n): { emails: AppTransactionalEmail[]; statsError: string | null } {\n const catalogs = [\n {\n appId: local.app,\n appName: local.app,\n appPath: \"\",\n emails: local.emails,\n error: null,\n statsError: local.statsAvailable ? null : local.statsError,\n },\n ...appCatalogs.filter((catalog) => catalog.appId !== local.app),\n ];\n const unreadable = catalogs.find(\n (catalog) => catalog.error || catalog.statsError,\n );\n const statsAvailable = !unreadable;\n\n return {\n statsError: unreadable?.error ?? unreadable?.statsError ?? null,\n emails: local.emails\n .filter((email) => email.app === \"core\")\n .map((definition) => {\n if (!statsAvailable) {\n return { ...definition, sent: null, failed: null, lastSentAt: null };\n }\n let sent = 0;\n let failed = 0;\n let lastSentAt: number | null = null;\n for (const catalog of catalogs) {\n const email = catalog.emails.find(\n (candidate) => candidate.id === definition.id,\n );\n sent += email?.sent ?? 0;\n failed += email?.failed ?? 0;\n if (\n email?.lastSentAt != null &&\n (lastSentAt === null || email.lastSentAt > lastSentAt)\n ) {\n lastSentAt = email.lastSentAt;\n }\n }\n return { ...definition, sent, failed, lastSentAt };\n }),\n };\n}\n\nfunction actionUrl(appPath: string, action: string, query = \"\"): string {\n const base = appPath.replace(/\\/$/, \"\");\n return `${base}/_agent-native/actions/${action}${query}`;\n}\n\nexport async function callAppAction<T>(\n appPath: string,\n action: string,\n params: Record<string, unknown>,\n method: \"GET\" | \"POST\",\n): Promise<T> {\n const query =\n method === \"GET\"\n ? `?${new URLSearchParams(\n Object.entries(params).map(([key, value]) => [key, String(value)]),\n )}`\n : \"\";\n const res = await fetch(actionUrl(appPath, action, query), {\n method,\n credentials: \"include\",\n headers: {\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\",\n \"X-Agent-Native-Frontend\": \"1\",\n },\n ...(method === \"POST\" ? { body: JSON.stringify(params) } : {}),\n });\n if (!res.ok) throw new Error(`${action} failed: HTTP ${res.status}`);\n return (await res.json()) as T;\n}\n\n/**\n * Load one app's catalog. Never throws — a single unreachable app must not\n * blank the whole screen, but its failure is returned rather than swallowed so\n * the UI can say \"couldn't read\" instead of showing it as having no emails.\n */\nconst CATALOG_TIMEOUT_MS = 10_000;\n\nexport async function fetchAppEmailCatalog(\n app: { id: string; name: string; path: string },\n windowDays: number,\n): Promise<AppEmailCatalog> {\n const base: AppEmailCatalog = {\n appId: app.id,\n appName: app.name,\n appPath: app.path,\n emails: [],\n error: null,\n statsError: null,\n };\n try {\n const res = await fetch(\n actionUrl(\n app.path,\n \"list-transactional-emails\",\n `?windowDays=${windowDays}`,\n ),\n {\n credentials: \"include\",\n headers: { Accept: \"application/json\" },\n signal: AbortSignal.timeout(CATALOG_TIMEOUT_MS),\n },\n );\n if (!res.ok) {\n return { ...base, error: `HTTP ${res.status}` };\n }\n const body = (await res.json()) as {\n emails?: AppTransactionalEmail[];\n statsAvailable?: boolean;\n statsError?: string | null;\n };\n return {\n ...base,\n emails: body.emails ?? [],\n statsError:\n body.statsAvailable === false ? (body.statsError ?? \"unknown\") : null,\n };\n } catch (error) {\n return {\n ...base,\n error: error instanceof Error ? error.message : String(error),\n };\n }\n}\n\nexport interface EmailPreview {\n subject: string;\n html: string;\n text: string;\n}\n\n/** Render one email's preview with dummy data, from the owning app. */\nexport async function fetchEmailPreview(\n appPath: string,\n id: string,\n): Promise<EmailPreview> {\n const res = await fetch(\n actionUrl(\n appPath,\n \"render-transactional-email-preview\",\n `?id=${encodeURIComponent(id)}`,\n ),\n { credentials: \"include\", headers: { Accept: \"application/json\" } },\n );\n if (!res.ok) {\n throw new Error(`Preview failed: HTTP ${res.status}`);\n }\n return (await res.json()) as EmailPreview;\n}\n"]}
1
+ {"version":3,"file":"transactional-emails.js","sourceRoot":"","sources":["../../src/client/transactional-emails.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Reads each mounted app's transactional email catalog from the browser.\n *\n * Workspace apps are path-mounted on a single origin, so the browser can call\n * another app's action endpoint directly and its own session cookie carries the\n * caller's identity. Fanning out here rather than server-side keeps every app's\n * existing access checks in force — Dispatch never sees a catalog the signed-in\n * user could not have loaded themselves.\n */\n\nexport interface AppTransactionalEmail {\n id: string;\n app: string;\n name: string;\n trigger: string;\n recipient: string;\n recipientLabel: string;\n sender: string;\n senderLabel: string;\n /** null when the send log could not be read — not the same as zero sends. */\n sent: number | null;\n failed: number | null;\n lastSentAt: number | null;\n}\n\nexport interface AppEmailCatalog {\n appId: string;\n appName: string;\n appPath: string;\n emails: AppTransactionalEmail[];\n /** Set when this app's catalog could not be read at all. */\n error: string | null;\n /** Set when the catalog loaded but its send counts did not. */\n statsError: string | null;\n}\n\nexport interface LocalTransactionalEmailCatalog {\n app: string;\n statsAvailable: boolean;\n statsError: string | null;\n emails: AppTransactionalEmail[];\n}\n\nexport function aggregateSharedEmails(\n local: LocalTransactionalEmailCatalog,\n appCatalogs: AppEmailCatalog[],\n): { emails: AppTransactionalEmail[]; statsError: string | null } {\n const catalogs = [\n {\n appId: local.app,\n appName: local.app,\n appPath: \"\",\n emails: local.emails,\n error: null,\n statsError: local.statsAvailable ? null : local.statsError,\n },\n ...appCatalogs.filter((catalog) => catalog.appId !== local.app),\n ];\n const unreadable = catalogs.find(\n (catalog) => catalog.error || catalog.statsError,\n );\n const statsAvailable = !unreadable;\n\n return {\n statsError: unreadable?.error ?? unreadable?.statsError ?? null,\n emails: local.emails\n .filter((email) => email.app === \"core\")\n .map((definition) => {\n if (!statsAvailable) {\n return { ...definition, sent: null, failed: null, lastSentAt: null };\n }\n let sent = 0;\n let failed = 0;\n let lastSentAt: number | null = null;\n for (const catalog of catalogs) {\n const email = catalog.emails.find(\n (candidate) => candidate.id === definition.id,\n );\n sent += email?.sent ?? 0;\n failed += email?.failed ?? 0;\n if (\n email?.lastSentAt != null &&\n (lastSentAt === null || email.lastSentAt > lastSentAt)\n ) {\n lastSentAt = email.lastSentAt;\n }\n }\n return { ...definition, sent, failed, lastSentAt };\n }),\n };\n}\n\nfunction actionUrl(appPath: string, action: string, query = \"\"): string {\n const base = appPath.replace(/\\/$/, \"\");\n return `${base}/_agent-native/actions/${action}${query}`;\n}\n\nexport async function callAppAction<T>(\n appPath: string,\n action: string,\n params: Record<string, unknown>,\n method: \"GET\" | \"POST\",\n): Promise<T> {\n const query =\n method === \"GET\"\n ? `?${new URLSearchParams(\n Object.entries(params).map(([key, value]) => [key, String(value)]),\n )}`\n : \"\";\n const res = await fetch(actionUrl(appPath, action, query), {\n method,\n credentials: \"include\",\n headers: {\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\",\n \"X-Agent-Native-Frontend\": \"1\",\n },\n ...(method === \"POST\" ? { body: JSON.stringify(params) } : {}),\n });\n if (!res.ok) {\n const error = new Error(`${action} failed: HTTP ${res.status}`);\n (error as Error & { status: number }).status = res.status;\n throw error;\n }\n return (await res.json()) as T;\n}\n\n/**\n * Load one app's catalog. Never throws — a single unreachable app must not\n * blank the whole screen, but its failure is returned rather than swallowed so\n * the UI can say \"couldn't read\" instead of showing it as having no emails.\n */\nconst CATALOG_TIMEOUT_MS = 10_000;\n\nexport async function fetchAppEmailCatalog(\n app: { id: string; name: string; path: string },\n windowDays: number,\n): Promise<AppEmailCatalog> {\n const base: AppEmailCatalog = {\n appId: app.id,\n appName: app.name,\n appPath: app.path,\n emails: [],\n error: null,\n statsError: null,\n };\n try {\n const res = await fetch(\n actionUrl(\n app.path,\n \"list-transactional-emails\",\n `?windowDays=${windowDays}`,\n ),\n {\n credentials: \"include\",\n headers: { Accept: \"application/json\" },\n signal: AbortSignal.timeout(CATALOG_TIMEOUT_MS),\n },\n );\n if (!res.ok) {\n return { ...base, error: `HTTP ${res.status}` };\n }\n const body = (await res.json()) as {\n emails?: AppTransactionalEmail[];\n statsAvailable?: boolean;\n statsError?: string | null;\n };\n return {\n ...base,\n emails: body.emails ?? [],\n statsError:\n body.statsAvailable === false ? (body.statsError ?? \"unknown\") : null,\n };\n } catch (error) {\n return {\n ...base,\n error: error instanceof Error ? error.message : String(error),\n };\n }\n}\n\nexport interface EmailPreview {\n subject: string;\n html: string;\n text: string;\n}\n\n/** Render one email's preview with dummy data, from the owning app. */\nexport async function fetchEmailPreview(\n appPath: string,\n id: string,\n): Promise<EmailPreview> {\n const res = await fetch(\n actionUrl(\n appPath,\n \"render-transactional-email-preview\",\n `?id=${encodeURIComponent(id)}`,\n ),\n { credentials: \"include\", headers: { Accept: \"application/json\" } },\n );\n if (!res.ok) {\n throw new Error(`Preview failed: HTTP ${res.status}`);\n }\n return (await res.json()) as EmailPreview;\n}\n"]}
@@ -0,0 +1,8 @@
1
+ export declare function SendLogSection({ apps, }: {
2
+ apps: {
3
+ id: string;
4
+ name: string;
5
+ path: string;
6
+ }[];
7
+ }): import("node_modules/@types/react").JSX.Element | null;
8
+ //# sourceMappingURL=transactional-email-send-log.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transactional-email-send-log.d.ts","sourceRoot":"","sources":["../../src/components/transactional-email-send-log.tsx"],"names":[],"mappings":"AA+GA,wBAAgB,cAAc,CAAC,EAC7B,IAAI,GACL,EAAE;IACD,IAAI,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CACpD,0DA2NA"}
@@ -0,0 +1,83 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useT } from "@agent-native/core/client/i18n";
3
+ import { DateRangePicker, dateRangeToInterval, } from "@agent-native/toolkit/dashboard";
4
+ import { useQuery } from "@tanstack/react-query";
5
+ import { useEffect, useState } from "react";
6
+ import { callAppAction } from "../client/transactional-emails.js";
7
+ import { ActionQueryError } from "./action-query-error.js";
8
+ import { Badge } from "./ui/badge.js";
9
+ import { Button } from "./ui/button.js";
10
+ import { Dialog, DialogContent, DialogHeader, DialogTitle } from "./ui/dialog.js";
11
+ import { Input } from "./ui/input.js";
12
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "./ui/select.js";
13
+ import { Skeleton } from "./ui/skeleton.js";
14
+ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "./ui/table.js";
15
+ /** One page of `list-email-log`, fetched at `PAGE_SIZE + 1` to detect "has more". */
16
+ const PAGE_SIZE = 50;
17
+ function useDebounced(value, delayMs = 300) {
18
+ const [debounced, setDebounced] = useState(value);
19
+ useEffect(() => {
20
+ const timer = setTimeout(() => setDebounced(value), delayMs);
21
+ return () => clearTimeout(timer);
22
+ }, [value, delayMs]);
23
+ return debounced;
24
+ }
25
+ function SendLogDetailDialog({ entry, open, onOpenChange, }) {
26
+ const t = useT();
27
+ if (!entry)
28
+ return null;
29
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "max-w-3xl", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: entry.subject }) }), _jsxs("div", { className: "grid grid-cols-2 gap-x-6 gap-y-1 text-xs", children: [_jsx("div", { className: "text-muted-foreground", children: t("dispatch.transactionalEmail.recipient") }), _jsx("div", { className: "font-mono", children: entry.recipient }), _jsx("div", { className: "text-muted-foreground", children: t("dispatch.transactionalEmail.sender") }), _jsx("div", { className: "font-mono", children: entry.sender }), _jsx("div", { className: "text-muted-foreground", children: t("dispatch.transactionalEmail.sendLogTemplate") }), _jsx("div", { className: "font-mono", children: entry.templateId ?? "—" }), _jsx("div", { className: "text-muted-foreground", children: t("dispatch.transactionalEmail.sendLogProvider") }), _jsx("div", { children: entry.provider }), _jsx("div", { className: "text-muted-foreground", children: t("dispatch.transactionalEmail.sendLogResponseStatus") }), _jsx("div", { children: entry.responseStatus ?? "—" }), entry.error ? (_jsxs(_Fragment, { children: [_jsx("div", { className: "text-muted-foreground", children: t("dispatch.transactionalEmail.sendLogError") }), _jsx("div", { className: "text-destructive", children: entry.error })] })) : null] })] }) }));
30
+ }
31
+ export function SendLogSection({ apps, }) {
32
+ const t = useT();
33
+ const [appId, setAppId] = useState(apps[0]?.id);
34
+ const [dateRange, setDateRange] = useState("7d");
35
+ const [to, setTo] = useState("");
36
+ const [from, setFrom] = useState("");
37
+ const [status, setStatus] = useState("all");
38
+ const [provider, setProvider] = useState("all");
39
+ const [offset, setOffset] = useState(0);
40
+ const [selected, setSelected] = useState(null);
41
+ const debouncedTo = useDebounced(to);
42
+ const debouncedFrom = useDebounced(from);
43
+ const selectedApp = apps.find((app) => app.id === appId) ?? apps[0];
44
+ useEffect(() => {
45
+ setOffset(0);
46
+ }, [appId, dateRange, debouncedTo, debouncedFrom, status, provider]);
47
+ const query = useQuery({
48
+ queryKey: [
49
+ "list-email-log",
50
+ selectedApp?.path,
51
+ dateRange,
52
+ debouncedTo,
53
+ debouncedFrom,
54
+ status,
55
+ provider,
56
+ offset,
57
+ ],
58
+ queryFn: () => callAppAction(selectedApp.path, "list-email-log", {
59
+ sinceMs: Date.now() - dateRangeToInterval(dateRange) * 86_400_000,
60
+ ...(debouncedTo ? { to: debouncedTo } : {}),
61
+ ...(debouncedFrom ? { from: debouncedFrom } : {}),
62
+ ...(status !== "all" ? { status } : {}),
63
+ ...(provider !== "all" ? { provider } : {}),
64
+ limit: PAGE_SIZE + 1,
65
+ offset,
66
+ }, "GET"),
67
+ enabled: Boolean(selectedApp),
68
+ });
69
+ const entries = (query.data?.entries ?? []).slice(0, PAGE_SIZE);
70
+ const hasMore = (query.data?.entries.length ?? 0) > PAGE_SIZE;
71
+ if (apps.length === 0)
72
+ return null;
73
+ return (_jsxs("div", { children: [_jsxs("div", { className: "mb-4 flex flex-wrap items-center gap-2", children: [_jsxs(Select, { value: selectedApp?.id, onValueChange: setAppId, children: [_jsx(SelectTrigger, { className: "w-48", children: _jsx(SelectValue, { placeholder: t("dispatch.transactionalEmail.app") }) }), _jsx(SelectContent, { children: apps.map((app) => (_jsx(SelectItem, { value: app.id, children: app.name }, app.id))) })] }), _jsx(DateRangePicker, { value: dateRange, onChange: setDateRange }), _jsx(Input, { placeholder: t("dispatch.transactionalEmail.sendLogToFilter"), value: to, onChange: (event) => setTo(event.target.value), className: "w-44" }), _jsx(Input, { placeholder: t("dispatch.transactionalEmail.sendLogFromFilter"), value: from, onChange: (event) => setFrom(event.target.value), className: "w-44" }), _jsxs(Select, { value: status, onValueChange: setStatus, children: [_jsx(SelectTrigger, { className: "w-32", children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "all", children: t("dispatch.transactionalEmail.sendLogAllStatuses") }), _jsx(SelectItem, { value: "sent", children: t("dispatch.transactionalEmail.sendLogSent") }), _jsx(SelectItem, { value: "failed", children: t("dispatch.transactionalEmail.sendLogFailed") })] })] }), _jsxs(Select, { value: provider, onValueChange: setProvider, children: [_jsx(SelectTrigger, { className: "w-36", children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "all", children: t("dispatch.transactionalEmail.sendLogAllProviders") }), _jsx(SelectItem, { value: "resend", children: "Resend" }), _jsx(SelectItem, { value: "sendgrid", children: "SendGrid" }), _jsx(SelectItem, { value: "dev", children: "Dev" })] })] }), (to || from || status !== "all" || provider !== "all") && (_jsx(Button, { variant: "ghost", size: "sm", onClick: () => {
74
+ setTo("");
75
+ setFrom("");
76
+ setStatus("all");
77
+ setProvider("all");
78
+ }, children: t("dispatch.transactionalEmail.sendLogClearFilters") }))] }), query.isError ? (_jsx(ActionQueryError, { error: query.error, onRetry: () => void query.refetch() })) : query.isLoading ? (_jsx(Skeleton, { className: "h-40 w-full" })) : entries.length === 0 ? (_jsx("div", { className: "rounded-xl border border-dashed px-4 py-8 text-center text-sm text-muted-foreground", children: t("dispatch.transactionalEmail.sendLogEmpty") })) : (_jsxs(_Fragment, { children: [_jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: t("dispatch.transactionalEmail.sendLogTimestamp") }), _jsx(TableHead, { children: t("dispatch.transactionalEmail.recipient") }), _jsx(TableHead, { children: t("dispatch.transactionalEmail.sender") }), _jsx(TableHead, { children: t("dispatch.transactionalEmail.sendLogTemplate") }), _jsx(TableHead, { children: t("dispatch.transactionalEmail.status") }), _jsx(TableHead, { children: t("dispatch.transactionalEmail.sendLogProvider") })] }) }), _jsx(TableBody, { children: entries.map((entry) => (_jsxs(TableRow, { className: "cursor-pointer", onClick: () => setSelected(entry), children: [_jsx(TableCell, { className: "text-xs text-muted-foreground", children: new Date(entry.createdAt).toLocaleString() }), _jsx(TableCell, { className: "text-xs", children: entry.recipient }), _jsx(TableCell, { className: "text-xs", children: entry.sender }), _jsx(TableCell, { className: "font-mono text-xs", children: entry.templateId ?? "—" }), _jsx(TableCell, { children: _jsx(Badge, { variant: entry.status === "failed" ? "destructive" : "secondary", children: entry.status }) }), _jsx(TableCell, { className: "text-xs", children: entry.provider })] }, entry.id))) })] }), _jsxs("div", { className: "mt-3 flex justify-end gap-2", children: [_jsx(Button, { variant: "outline", size: "sm", disabled: offset === 0, onClick: () => setOffset((current) => Math.max(0, current - PAGE_SIZE)), children: t("dispatch.transactionalEmail.sendLogPrevious") }), _jsx(Button, { variant: "outline", size: "sm", disabled: !hasMore, onClick: () => setOffset((current) => current + PAGE_SIZE), children: t("dispatch.transactionalEmail.sendLogNext") })] })] })), _jsx(SendLogDetailDialog, { entry: selected, open: selected !== null, onOpenChange: (next) => {
79
+ if (!next)
80
+ setSelected(null);
81
+ } })] }));
82
+ }
83
+ //# sourceMappingURL=transactional-email-send-log.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transactional-email-send-log.js","sourceRoot":"","sources":["../../src/components/transactional-email-send-log.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useT } from \"@agent-native/core/client/i18n\";\nimport {\n DateRangePicker,\n dateRangeToInterval,\n type DateRange,\n} from \"@agent-native/toolkit/dashboard\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { useEffect, useState } from \"react\";\n\nimport { callAppAction } from \"../client/transactional-emails\";\nimport { ActionQueryError } from \"./action-query-error\";\nimport { Badge } from \"./ui/badge\";\nimport { Button } from \"./ui/button\";\nimport { Dialog, DialogContent, DialogHeader, DialogTitle } from \"./ui/dialog\";\nimport { Input } from \"./ui/input\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"./ui/select\";\nimport { Skeleton } from \"./ui/skeleton\";\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from \"./ui/table\";\n\ninterface SendLogEntry {\n id: string;\n templateId: string | null;\n app: string | null;\n recipient: string;\n sender: string;\n subject: string;\n status: string;\n error: string | null;\n provider: string;\n responseStatus: number | null;\n createdAt: number;\n}\n\n/** One page of `list-email-log`, fetched at `PAGE_SIZE + 1` to detect \"has more\". */\nconst PAGE_SIZE = 50;\n\nfunction useDebounced(value: string, delayMs = 300): string {\n const [debounced, setDebounced] = useState(value);\n useEffect(() => {\n const timer = setTimeout(() => setDebounced(value), delayMs);\n return () => clearTimeout(timer);\n }, [value, delayMs]);\n return debounced;\n}\n\nfunction SendLogDetailDialog({\n entry,\n open,\n onOpenChange,\n}: {\n entry: SendLogEntry | null;\n open: boolean;\n onOpenChange: (next: boolean) => void;\n}) {\n const t = useT();\n if (!entry) return null;\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent className=\"max-w-3xl\">\n <DialogHeader>\n <DialogTitle>{entry.subject}</DialogTitle>\n </DialogHeader>\n <div className=\"grid grid-cols-2 gap-x-6 gap-y-1 text-xs\">\n <div className=\"text-muted-foreground\">\n {t(\"dispatch.transactionalEmail.recipient\")}\n </div>\n <div className=\"font-mono\">{entry.recipient}</div>\n <div className=\"text-muted-foreground\">\n {t(\"dispatch.transactionalEmail.sender\")}\n </div>\n <div className=\"font-mono\">{entry.sender}</div>\n <div className=\"text-muted-foreground\">\n {t(\"dispatch.transactionalEmail.sendLogTemplate\")}\n </div>\n <div className=\"font-mono\">{entry.templateId ?? \"—\"}</div>\n <div className=\"text-muted-foreground\">\n {t(\"dispatch.transactionalEmail.sendLogProvider\")}\n </div>\n <div>{entry.provider}</div>\n <div className=\"text-muted-foreground\">\n {t(\"dispatch.transactionalEmail.sendLogResponseStatus\")}\n </div>\n <div>{entry.responseStatus ?? \"—\"}</div>\n {entry.error ? (\n <>\n <div className=\"text-muted-foreground\">\n {t(\"dispatch.transactionalEmail.sendLogError\")}\n </div>\n <div className=\"text-destructive\">{entry.error}</div>\n </>\n ) : null}\n </div>\n </DialogContent>\n </Dialog>\n );\n}\n\nexport function SendLogSection({\n apps,\n}: {\n apps: { id: string; name: string; path: string }[];\n}) {\n const t = useT();\n const [appId, setAppId] = useState<string | undefined>(apps[0]?.id);\n const [dateRange, setDateRange] = useState<DateRange>(\"7d\");\n const [to, setTo] = useState(\"\");\n const [from, setFrom] = useState(\"\");\n const [status, setStatus] = useState<string>(\"all\");\n const [provider, setProvider] = useState<string>(\"all\");\n const [offset, setOffset] = useState(0);\n const [selected, setSelected] = useState<SendLogEntry | null>(null);\n\n const debouncedTo = useDebounced(to);\n const debouncedFrom = useDebounced(from);\n const selectedApp = apps.find((app) => app.id === appId) ?? apps[0];\n\n useEffect(() => {\n setOffset(0);\n }, [appId, dateRange, debouncedTo, debouncedFrom, status, provider]);\n\n const query = useQuery({\n queryKey: [\n \"list-email-log\",\n selectedApp?.path,\n dateRange,\n debouncedTo,\n debouncedFrom,\n status,\n provider,\n offset,\n ],\n queryFn: () =>\n callAppAction<{ entries: SendLogEntry[] }>(\n selectedApp!.path,\n \"list-email-log\",\n {\n sinceMs: Date.now() - dateRangeToInterval(dateRange) * 86_400_000,\n ...(debouncedTo ? { to: debouncedTo } : {}),\n ...(debouncedFrom ? { from: debouncedFrom } : {}),\n ...(status !== \"all\" ? { status } : {}),\n ...(provider !== \"all\" ? { provider } : {}),\n limit: PAGE_SIZE + 1,\n offset,\n },\n \"GET\",\n ),\n enabled: Boolean(selectedApp),\n });\n\n const entries = (query.data?.entries ?? []).slice(0, PAGE_SIZE);\n const hasMore = (query.data?.entries.length ?? 0) > PAGE_SIZE;\n\n if (apps.length === 0) return null;\n\n return (\n <div>\n <div className=\"mb-4 flex flex-wrap items-center gap-2\">\n <Select value={selectedApp?.id} onValueChange={setAppId}>\n <SelectTrigger className=\"w-48\">\n <SelectValue placeholder={t(\"dispatch.transactionalEmail.app\")} />\n </SelectTrigger>\n <SelectContent>\n {apps.map((app) => (\n <SelectItem key={app.id} value={app.id}>\n {app.name}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n <DateRangePicker value={dateRange} onChange={setDateRange} />\n <Input\n placeholder={t(\"dispatch.transactionalEmail.sendLogToFilter\")}\n value={to}\n onChange={(event) => setTo(event.target.value)}\n className=\"w-44\"\n />\n <Input\n placeholder={t(\"dispatch.transactionalEmail.sendLogFromFilter\")}\n value={from}\n onChange={(event) => setFrom(event.target.value)}\n className=\"w-44\"\n />\n <Select value={status} onValueChange={setStatus}>\n <SelectTrigger className=\"w-32\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"all\">\n {t(\"dispatch.transactionalEmail.sendLogAllStatuses\")}\n </SelectItem>\n <SelectItem value=\"sent\">\n {t(\"dispatch.transactionalEmail.sendLogSent\")}\n </SelectItem>\n <SelectItem value=\"failed\">\n {t(\"dispatch.transactionalEmail.sendLogFailed\")}\n </SelectItem>\n </SelectContent>\n </Select>\n <Select value={provider} onValueChange={setProvider}>\n <SelectTrigger className=\"w-36\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"all\">\n {t(\"dispatch.transactionalEmail.sendLogAllProviders\")}\n </SelectItem>\n <SelectItem value=\"resend\">Resend</SelectItem>\n <SelectItem value=\"sendgrid\">SendGrid</SelectItem>\n <SelectItem value=\"dev\">Dev</SelectItem>\n </SelectContent>\n </Select>\n {(to || from || status !== \"all\" || provider !== \"all\") && (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => {\n setTo(\"\");\n setFrom(\"\");\n setStatus(\"all\");\n setProvider(\"all\");\n }}\n >\n {t(\"dispatch.transactionalEmail.sendLogClearFilters\")}\n </Button>\n )}\n </div>\n\n {query.isError ? (\n <ActionQueryError\n error={query.error}\n onRetry={() => void query.refetch()}\n />\n ) : query.isLoading ? (\n <Skeleton className=\"h-40 w-full\" />\n ) : entries.length === 0 ? (\n <div className=\"rounded-xl border border-dashed px-4 py-8 text-center text-sm text-muted-foreground\">\n {t(\"dispatch.transactionalEmail.sendLogEmpty\")}\n </div>\n ) : (\n <>\n <Table>\n <TableHeader>\n <TableRow>\n <TableHead>\n {t(\"dispatch.transactionalEmail.sendLogTimestamp\")}\n </TableHead>\n <TableHead>\n {t(\"dispatch.transactionalEmail.recipient\")}\n </TableHead>\n <TableHead>{t(\"dispatch.transactionalEmail.sender\")}</TableHead>\n <TableHead>\n {t(\"dispatch.transactionalEmail.sendLogTemplate\")}\n </TableHead>\n <TableHead>{t(\"dispatch.transactionalEmail.status\")}</TableHead>\n <TableHead>\n {t(\"dispatch.transactionalEmail.sendLogProvider\")}\n </TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {entries.map((entry) => (\n <TableRow\n key={entry.id}\n className=\"cursor-pointer\"\n onClick={() => setSelected(entry)}\n >\n <TableCell className=\"text-xs text-muted-foreground\">\n {new Date(entry.createdAt).toLocaleString()}\n </TableCell>\n <TableCell className=\"text-xs\">{entry.recipient}</TableCell>\n <TableCell className=\"text-xs\">{entry.sender}</TableCell>\n <TableCell className=\"font-mono text-xs\">\n {entry.templateId ?? \"—\"}\n </TableCell>\n <TableCell>\n <Badge\n variant={\n entry.status === \"failed\" ? \"destructive\" : \"secondary\"\n }\n >\n {entry.status}\n </Badge>\n </TableCell>\n <TableCell className=\"text-xs\">{entry.provider}</TableCell>\n </TableRow>\n ))}\n </TableBody>\n </Table>\n <div className=\"mt-3 flex justify-end gap-2\">\n <Button\n variant=\"outline\"\n size=\"sm\"\n disabled={offset === 0}\n onClick={() =>\n setOffset((current) => Math.max(0, current - PAGE_SIZE))\n }\n >\n {t(\"dispatch.transactionalEmail.sendLogPrevious\")}\n </Button>\n <Button\n variant=\"outline\"\n size=\"sm\"\n disabled={!hasMore}\n onClick={() => setOffset((current) => current + PAGE_SIZE)}\n >\n {t(\"dispatch.transactionalEmail.sendLogNext\")}\n </Button>\n </div>\n </>\n )}\n\n <SendLogDetailDialog\n entry={selected}\n open={selected !== null}\n onOpenChange={(next) => {\n if (!next) setSelected(null);\n }}\n />\n </div>\n );\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"transactional-email.d.ts","sourceRoot":"","sources":["../../../src/routes/pages/transactional-email.tsx"],"names":[],"mappings":"AA4DA,wBAAgB,IAAI;;IAEnB;AA6UD,MAAM,CAAC,OAAO,UAAU,uBAAuB,oDAmL9C"}
1
+ {"version":3,"file":"transactional-email.d.ts","sourceRoot":"","sources":["../../../src/routes/pages/transactional-email.tsx"],"names":[],"mappings":"AA6DA,wBAAgB,IAAI;;IAEnB;AA6UD,MAAM,CAAC,OAAO,UAAU,uBAAuB,oDAmM9C"}
@@ -11,6 +11,7 @@ import { DispatchShell } from "../../components/dispatch-shell.js";
11
11
  import { ActivityTable, } from "../../components/transactional-email-activity.js";
12
12
  import { OpenRateCell, SendsCell, LastSentCell, } from "../../components/transactional-email-metrics.js";
13
13
  import { EmailPreviewPane } from "../../components/transactional-email-preview.js";
14
+ import { SendLogSection } from "../../components/transactional-email-send-log.js";
14
15
  import { Alert, AlertDescription, AlertTitle } from "../../components/ui/alert.js";
15
16
  import { Button } from "../../components/ui/button.js";
16
17
  import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from "../../components/ui/collapsible.js";
@@ -116,7 +117,7 @@ export default function TransactionalEmailRoute() {
116
117
  (sharedOpen && catalogQueries.some((query) => query.isLoading));
117
118
  const sharedError = catalogQueries.find((query) => query.isError)?.error;
118
119
  const sharedProviderReason = t("dispatch.transactionalEmail.sharedProviderMetricsUnavailable");
119
- return (_jsx(DispatchShell, { title: t("dispatch.transactionalEmail.title"), description: t("dispatch.transactionalEmail.description"), children: _jsxs("div", { className: "flex flex-col gap-4", children: [_jsxs(Alert, { children: [_jsx(IconInfoCircle, { className: "size-4" }), _jsx(AlertTitle, { children: t("dispatch.transactionalEmail.retentionTitle") }), _jsx(AlertDescription, { children: t("dispatch.transactionalEmail.retentionNote") })] }), appsQuery.isError ? (_jsx(ActionQueryError, { error: appsQuery.error, onRetry: () => void appsQuery.refetch() })) : null, appsQuery.isLoading ? (_jsxs("div", { className: "space-y-3", children: [_jsx(Skeleton, { className: "h-16 w-full rounded-2xl" }), _jsx(Skeleton, { className: "h-16 w-full rounded-2xl" })] })) : null, !appsQuery.isLoading && apps.length === 0 ? (_jsxs("div", { className: "rounded-2xl border border-dashed px-6 py-12 text-center text-sm text-muted-foreground", children: [_jsx(IconMail, { className: "mx-auto mb-2 size-5" }), t("dispatch.transactionalEmail.noApps")] })) : null, _jsx(AppEmailCard, { name: t("dispatch.transactionalEmail.sharedTitle"), path: t("dispatch.transactionalEmail.sharedSubtitle"), open: sharedOpen, onOpenChange: setSharedOpen, catalog: sharedCatalog, isLoading: sharedLoading, isError: sharedQuery.isError || Boolean(sharedError), error: sharedQuery.error ?? sharedError, onRetry: () => {
120
+ return (_jsx(DispatchShell, { title: t("dispatch.transactionalEmail.title"), description: t("dispatch.transactionalEmail.description"), children: _jsxs("div", { className: "flex flex-col gap-4", children: [_jsxs(Alert, { children: [_jsx(IconInfoCircle, { className: "size-4" }), _jsx(AlertTitle, { children: t("dispatch.transactionalEmail.retentionTitle") }), _jsx(AlertDescription, { children: t("dispatch.transactionalEmail.retentionNote") })] }), appsQuery.isError ? (_jsx(ActionQueryError, { error: appsQuery.error, onRetry: () => void appsQuery.refetch() })) : null, appsQuery.isLoading ? (_jsxs("div", { className: "space-y-3", children: [_jsx(Skeleton, { className: "h-16 w-full rounded-2xl" }), _jsx(Skeleton, { className: "h-16 w-full rounded-2xl" })] })) : null, !appsQuery.isLoading && apps.length === 0 ? (_jsxs("div", { className: "rounded-2xl border border-dashed px-6 py-12 text-center text-sm text-muted-foreground", children: [_jsx(IconMail, { className: "mx-auto mb-2 size-5" }), t("dispatch.transactionalEmail.noApps")] })) : null, apps.length > 0 ? (_jsxs(Collapsible, { defaultOpen: true, className: "rounded-2xl bg-card", children: [_jsx(CollapsibleTrigger, { className: "group flex w-full cursor-pointer items-center justify-between gap-3 p-5 text-left hover:bg-muted/20", children: _jsxs("span", { className: "flex items-center gap-2", children: [_jsx(IconChevronRight, { className: "h-4 w-4 shrink-0 text-muted-foreground transition-transform group-data-[state=open]:rotate-90" }), _jsx("span", { className: "text-sm font-medium text-foreground", children: t("dispatch.transactionalEmail.sendLogTitle") })] }) }), _jsx(CollapsibleContent, { className: "border-t px-5 pb-5 pt-4", children: _jsx(SendLogSection, { apps: apps }) })] })) : null, _jsx(AppEmailCard, { name: t("dispatch.transactionalEmail.sharedTitle"), path: t("dispatch.transactionalEmail.sharedSubtitle"), open: sharedOpen, onOpenChange: setSharedOpen, catalog: sharedCatalog, isLoading: sharedLoading, isError: sharedQuery.isError || Boolean(sharedError), error: sharedQuery.error ?? sharedError, onRetry: () => {
120
121
  void sharedQuery.refetch();
121
122
  for (const query of catalogQueries)
122
123
  void query.refetch();
@@ -1 +1 @@
1
- {"version":3,"file":"transactional-email.js","sourceRoot":"","sources":["../../../src/routes/pages/transactional-email.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useActionQuery } from \"@agent-native/core/client/hooks\";\nimport { useT } from \"@agent-native/core/client/i18n\";\nimport {\n IconAlertTriangle,\n IconChevronRight,\n IconEye,\n IconInfoCircle,\n IconList,\n IconMail,\n} from \"@tabler/icons-react\";\nimport { useQueries, useQuery } from \"@tanstack/react-query\";\nimport { useMemo, useState } from \"react\";\nimport { Link, useLocation } from \"react-router\";\n\nimport {\n aggregateSharedEmails,\n callAppAction,\n fetchAppEmailCatalog,\n type AppEmailCatalog,\n type AppTransactionalEmail,\n type LocalTransactionalEmailCatalog,\n} from \"../../client/transactional-emails\";\nimport { ActionQueryError } from \"../../components/action-query-error\";\nimport { DispatchShell } from \"../../components/dispatch-shell\";\nimport {\n ActivityTable,\n type EmailActivityEntry,\n} from \"../../components/transactional-email-activity\";\nimport {\n OpenRateCell,\n SendsCell,\n LastSentCell,\n type EmailEngagement,\n type ProviderMetricsResult,\n} from \"../../components/transactional-email-metrics\";\nimport { EmailPreviewPane } from \"../../components/transactional-email-preview\";\nimport { Alert, AlertDescription, AlertTitle } from \"../../components/ui/alert\";\nimport { Button } from \"../../components/ui/button\";\nimport {\n Collapsible,\n CollapsibleContent,\n CollapsibleTrigger,\n} from \"../../components/ui/collapsible\";\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogHeader,\n DialogTitle,\n} from \"../../components/ui/dialog\";\nimport { Skeleton } from \"../../components/ui/skeleton\";\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from \"../../components/ui/table\";\n\nexport function meta() {\n return [{ title: \"Transactional email — Dispatch\" }];\n}\n\nconst WINDOW_DAYS = 30;\nconst ACTIVITY_LIMIT = 50;\n\ninterface WorkspaceAppRef {\n id: string;\n name: string;\n path: string;\n status?: \"ready\" | \"pending\";\n}\n\n/** Shape of the local `list-transactional-emails` action response. */\nfunction PreviewDialog({\n email,\n appId,\n appPath,\n open,\n onOpenChange,\n}: {\n email: AppTransactionalEmail;\n appId: string;\n appPath: string;\n open: boolean;\n onOpenChange: (next: boolean) => void;\n}) {\n const t = useT();\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent className=\"max-w-3xl\">\n <DialogHeader>\n <DialogTitle>{email.name}</DialogTitle>\n <DialogDescription>\n {t(\"dispatch.transactionalEmail.previewDescription\")}\n </DialogDescription>\n </DialogHeader>\n {/* Dialog content only mounts while open, so this fetches on open and\n drops the request when closed rather than needing an enabled flag. */}\n <EmailPreviewPane\n appId={appId}\n appPath={appPath}\n id={email.id}\n name={email.name}\n />\n </DialogContent>\n </Dialog>\n );\n}\n\nfunction ActivityDialog({\n email,\n appPath,\n unavailableReason,\n open,\n onOpenChange,\n}: {\n email: AppTransactionalEmail;\n appPath: string;\n unavailableReason: string | null;\n open: boolean;\n onOpenChange: (next: boolean) => void;\n}) {\n const t = useT();\n const activityQuery = useQuery({\n queryKey: [\"list-email-activity\", appPath, email.id, ACTIVITY_LIMIT],\n queryFn: () =>\n callAppAction<ProviderMetricsResult<EmailActivityEntry[]>>(\n appPath,\n \"list-email-activity\",\n { templateId: email.id, limit: ACTIVITY_LIMIT },\n \"GET\",\n ),\n enabled: open && !unavailableReason,\n });\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent className=\"max-w-4xl\">\n <DialogHeader>\n <DialogTitle>\n {t(\"dispatch.transactionalEmail.activityTitle\", {\n name: email.name,\n })}\n </DialogTitle>\n <DialogDescription>\n {t(\"dispatch.transactionalEmail.retentionNote\")}\n </DialogDescription>\n </DialogHeader>\n <ActivityTable\n result={\n unavailableReason\n ? { available: false, reason: unavailableReason }\n : activityQuery.data\n }\n isLoading={activityQuery.isLoading}\n isError={activityQuery.isError}\n error={activityQuery.error}\n onRetry={() => void activityQuery.refetch()}\n />\n </DialogContent>\n </Dialog>\n );\n}\n\nfunction EmailRow({\n email,\n appId,\n appPath,\n engagement,\n engagementUnavailable,\n engagementLoading,\n}: {\n email: AppTransactionalEmail;\n appId: string;\n appPath: string;\n engagement: EmailEngagement | undefined;\n engagementUnavailable: string | null;\n engagementLoading: boolean;\n}) {\n const t = useT();\n const location = useLocation();\n const [previewOpen, setPreviewOpen] = useState(false);\n const [activityOpen, setActivityOpen] = useState(false);\n const detailBase = location.pathname.startsWith(\"/admin/\")\n ? \"/admin/transactional-email\"\n : \"/transactional-email\";\n\n return (\n <TableRow>\n <TableCell className=\"align-top\">\n <Link\n to={`${detailBase}/${appId}/${email.id}`}\n className=\"text-sm font-medium text-foreground hover:underline\"\n >\n {email.name}\n </Link>\n <div className=\"font-mono text-xs text-muted-foreground\">\n {email.id}\n </div>\n </TableCell>\n <TableCell className=\"max-w-64 align-top text-xs text-muted-foreground\">\n {email.trigger}\n </TableCell>\n <TableCell className=\"max-w-56 align-top text-xs text-muted-foreground\">\n {email.recipientLabel}\n </TableCell>\n <TableCell className=\"max-w-56 align-top text-xs text-muted-foreground\">\n {email.senderLabel}\n </TableCell>\n <TableCell className=\"align-top text-sm\">\n <SendsCell sent={email.sent} failed={email.failed} />\n </TableCell>\n <TableCell className=\"align-top text-sm\">\n <OpenRateCell\n engagement={engagement}\n unavailableReason={engagementUnavailable}\n loading={engagementLoading}\n />\n </TableCell>\n <TableCell className=\"align-top text-xs text-muted-foreground\">\n <LastSentCell lastSentAt={email.lastSentAt} sent={email.sent} />\n </TableCell>\n <TableCell className=\"align-top\">\n <div className=\"flex justify-end gap-2\">\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => setPreviewOpen(true)}\n >\n <IconEye className=\"size-4\" />\n {t(\"dispatch.transactionalEmail.preview\")}\n </Button>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => setActivityOpen(true)}\n >\n <IconList className=\"size-4\" />\n {t(\"dispatch.transactionalEmail.activityLink\")}\n </Button>\n </div>\n <PreviewDialog\n email={email}\n appId={appId}\n appPath={appPath}\n open={previewOpen}\n onOpenChange={setPreviewOpen}\n />\n <ActivityDialog\n email={email}\n appPath={appPath}\n unavailableReason={engagementUnavailable}\n open={activityOpen}\n onOpenChange={setActivityOpen}\n />\n </TableCell>\n </TableRow>\n );\n}\n\nfunction AppEmailTable({\n catalog,\n engagementByTemplate,\n engagementUnavailable,\n engagementLoading,\n}: {\n catalog: AppEmailCatalog;\n engagementByTemplate: Map<string, EmailEngagement>;\n engagementUnavailable: string | null;\n engagementLoading: boolean;\n}) {\n const t = useT();\n\n if (catalog.error) {\n return (\n <Alert variant=\"destructive\">\n <IconAlertTriangle className=\"size-4\" />\n <AlertTitle>\n {t(\"dispatch.transactionalEmail.catalogUnreadable\")}\n </AlertTitle>\n <AlertDescription>{catalog.error}</AlertDescription>\n </Alert>\n );\n }\n\n return (\n <>\n {catalog.statsError ? (\n <Alert className=\"mb-4\">\n <IconInfoCircle className=\"size-4\" />\n <AlertTitle>\n {t(\"dispatch.transactionalEmail.countsUnreadable\")}\n </AlertTitle>\n <AlertDescription>{catalog.statsError}</AlertDescription>\n </Alert>\n ) : null}\n {catalog.emails.length === 0 ? (\n <div className=\"rounded-xl border border-dashed px-4 py-6 text-sm text-muted-foreground\">\n {t(\"dispatch.transactionalEmail.appSendsNoEmail\")}\n </div>\n ) : (\n <Table>\n <TableHeader>\n <TableRow>\n <TableHead>{t(\"dispatch.transactionalEmail.email\")}</TableHead>\n <TableHead>{t(\"dispatch.transactionalEmail.trigger\")}</TableHead>\n <TableHead>\n {t(\"dispatch.transactionalEmail.recipient\")}\n </TableHead>\n <TableHead>{t(\"dispatch.transactionalEmail.sender\")}</TableHead>\n <TableHead>{t(\"dispatch.transactionalEmail.sends\")}</TableHead>\n <TableHead>{t(\"dispatch.transactionalEmail.openRate\")}</TableHead>\n <TableHead>{t(\"dispatch.transactionalEmail.lastSent\")}</TableHead>\n <TableHead />\n </TableRow>\n </TableHeader>\n <TableBody>\n {catalog.emails.map((email) => (\n <EmailRow\n key={email.id}\n email={email}\n appId={catalog.appId}\n appPath={catalog.appPath}\n engagement={engagementByTemplate.get(email.id)}\n engagementUnavailable={engagementUnavailable}\n engagementLoading={engagementLoading}\n />\n ))}\n </TableBody>\n </Table>\n )}\n </>\n );\n}\n\nfunction AppEmailCard({\n name,\n path,\n open,\n onOpenChange,\n catalog,\n isLoading,\n isError,\n error,\n onRetry,\n engagementByTemplate,\n engagementUnavailable,\n engagementLoading,\n}: {\n name: string;\n path: string;\n open: boolean;\n onOpenChange: (next: boolean) => void;\n catalog: AppEmailCatalog | undefined;\n isLoading: boolean;\n isError: boolean;\n error: unknown;\n onRetry: () => void;\n engagementByTemplate: Map<string, EmailEngagement>;\n engagementUnavailable: string | null;\n engagementLoading: boolean;\n}) {\n return (\n <Collapsible\n open={open}\n onOpenChange={onOpenChange}\n className=\"rounded-2xl bg-card\"\n >\n <CollapsibleTrigger className=\"group flex w-full cursor-pointer items-center justify-between gap-3 p-5 text-left hover:bg-muted/20\">\n <span className=\"flex items-center gap-2\">\n <IconChevronRight className=\"h-4 w-4 shrink-0 text-muted-foreground transition-transform group-data-[state=open]:rotate-90\" />\n <span className=\"text-sm font-medium text-foreground\">{name}</span>\n </span>\n <span className=\"font-mono text-xs text-muted-foreground\">{path}</span>\n </CollapsibleTrigger>\n <CollapsibleContent className=\"border-t px-5 pb-5 pt-4\">\n {isError ? (\n <ActionQueryError error={error} onRetry={onRetry} />\n ) : isLoading || !catalog ? (\n <Skeleton className=\"h-24 w-full\" />\n ) : (\n <AppEmailTable\n catalog={catalog}\n engagementByTemplate={engagementByTemplate}\n engagementUnavailable={engagementUnavailable}\n engagementLoading={engagementLoading}\n />\n )}\n </CollapsibleContent>\n </Collapsible>\n );\n}\n\nexport default function TransactionalEmailRoute() {\n const t = useT();\n const appsQuery = useActionQuery<WorkspaceAppRef[]>(\"list-workspace-apps\", {\n includeAgentCards: false,\n });\n\n const apps = useMemo(\n () =>\n (appsQuery.data ?? [])\n .filter((app) => app.status !== \"pending\")\n .map((app) => ({ id: app.id, name: app.name, path: app.path })),\n [appsQuery.data],\n );\n\n const [expanded, setExpanded] = useState<Record<string, boolean>>({});\n const [sharedOpen, setSharedOpen] = useState(true);\n\n const sharedQuery = useActionQuery<LocalTransactionalEmailCatalog>(\n \"list-transactional-emails\",\n { windowDays: WINDOW_DAYS },\n );\n\n const catalogQueries = useQueries({\n queries: apps.map((app) => ({\n queryKey: [\"transactional-email-catalog\", app.id, WINDOW_DAYS],\n queryFn: () => fetchAppEmailCatalog(app, WINDOW_DAYS),\n enabled: sharedOpen || expanded[app.id] === true,\n staleTime: Infinity,\n })),\n });\n\n const sharedCatalog = useMemo<AppEmailCatalog | undefined>(() => {\n if (!sharedQuery.data) return undefined;\n const aggregate = aggregateSharedEmails(\n sharedQuery.data,\n catalogQueries\n .map((query) => query.data)\n .filter((catalog): catalog is AppEmailCatalog => Boolean(catalog)),\n );\n return {\n appId: \"core\",\n appName: t(\"dispatch.transactionalEmail.sharedTitle\"),\n appPath: t(\"dispatch.transactionalEmail.sharedSubtitle\"),\n emails: aggregate.emails,\n error: null,\n statsError: aggregate.statsError,\n };\n }, [sharedQuery.data, catalogQueries, t]);\n\n const engagementQueries = useQueries({\n queries: apps.map((app, index) => {\n const catalog = catalogQueries[index]?.data;\n const templateIds =\n catalog && !catalog.error\n ? catalog.emails.map((email) => email.id)\n : [];\n return {\n queryKey: [\n \"list-email-engagement\",\n app.id,\n templateIds.join(\",\"),\n WINDOW_DAYS,\n ],\n queryFn: () =>\n callAppAction<ProviderMetricsResult<EmailEngagement[]>>(\n app.path,\n \"list-email-engagement\",\n { templateIds, windowDays: WINDOW_DAYS },\n \"POST\",\n ),\n enabled: expanded[app.id] === true && templateIds.length > 0,\n };\n }),\n });\n\n const sharedLoading =\n sharedQuery.isLoading ||\n (sharedOpen && catalogQueries.some((query) => query.isLoading));\n const sharedError = catalogQueries.find((query) => query.isError)?.error;\n const sharedProviderReason = t(\n \"dispatch.transactionalEmail.sharedProviderMetricsUnavailable\",\n );\n\n return (\n <DispatchShell\n title={t(\"dispatch.transactionalEmail.title\")}\n description={t(\"dispatch.transactionalEmail.description\")}\n >\n <div className=\"flex flex-col gap-4\">\n <Alert>\n <IconInfoCircle className=\"size-4\" />\n <AlertTitle>\n {t(\"dispatch.transactionalEmail.retentionTitle\")}\n </AlertTitle>\n <AlertDescription>\n {t(\"dispatch.transactionalEmail.retentionNote\")}\n </AlertDescription>\n </Alert>\n\n {appsQuery.isError ? (\n <ActionQueryError\n error={appsQuery.error}\n onRetry={() => void appsQuery.refetch()}\n />\n ) : null}\n\n {appsQuery.isLoading ? (\n <div className=\"space-y-3\">\n <Skeleton className=\"h-16 w-full rounded-2xl\" />\n <Skeleton className=\"h-16 w-full rounded-2xl\" />\n </div>\n ) : null}\n\n {!appsQuery.isLoading && apps.length === 0 ? (\n <div className=\"rounded-2xl border border-dashed px-6 py-12 text-center text-sm text-muted-foreground\">\n <IconMail className=\"mx-auto mb-2 size-5\" />\n {t(\"dispatch.transactionalEmail.noApps\")}\n </div>\n ) : null}\n\n <AppEmailCard\n name={t(\"dispatch.transactionalEmail.sharedTitle\")}\n path={t(\"dispatch.transactionalEmail.sharedSubtitle\")}\n open={sharedOpen}\n onOpenChange={setSharedOpen}\n catalog={sharedCatalog}\n isLoading={sharedLoading}\n isError={sharedQuery.isError || Boolean(sharedError)}\n error={sharedQuery.error ?? sharedError}\n onRetry={() => {\n void sharedQuery.refetch();\n for (const query of catalogQueries) void query.refetch();\n }}\n engagementByTemplate={new Map()}\n engagementUnavailable={sharedProviderReason}\n engagementLoading={false}\n />\n\n {apps.map((app, index) => {\n const query = catalogQueries[index];\n const engagementQuery = engagementQueries[index];\n const engagementResult = engagementQuery?.data;\n const engagementUnavailable =\n engagementResult && !engagementResult.available\n ? engagementResult.reason\n : engagementQuery?.isError\n ? engagementQuery.error instanceof Error\n ? engagementQuery.error.message\n : String(engagementQuery.error)\n : null;\n const engagementByTemplate = new Map<string, EmailEngagement>();\n if (engagementResult?.available) {\n for (const entry of engagementResult.data) {\n engagementByTemplate.set(entry.templateId, entry);\n }\n }\n return (\n <AppEmailCard\n key={app.id}\n name={app.name}\n path={app.path}\n open={expanded[app.id] === true}\n onOpenChange={(next) =>\n setExpanded((current) => ({ ...current, [app.id]: next }))\n }\n catalog={query?.data}\n isLoading={query?.isLoading ?? false}\n isError={query?.isError ?? false}\n error={query?.error}\n onRetry={() => void query?.refetch()}\n engagementByTemplate={engagementByTemplate}\n engagementUnavailable={engagementUnavailable}\n engagementLoading={engagementQuery?.isLoading ?? false}\n />\n );\n })}\n </div>\n </DispatchShell>\n );\n}\n"]}
1
+ {"version":3,"file":"transactional-email.js","sourceRoot":"","sources":["../../../src/routes/pages/transactional-email.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AACjE,OAAO,EAAE,IAAI,EAAE,MAAM,gCAAgC,CAAC;AACtD,OAAO,EACL,iBAAiB,EACjB,gBAAgB,EAChB,OAAO,EACP,cAAc,EACd,QAAQ,EACR,QAAQ,GACT,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAC1C,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEjD,OAAO,EACL,qBAAqB,EACrB,aAAa,EACb,oBAAoB,GAIrB,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EAAE,gBAAgB,EAAE,MAAM,qCAAqC,CAAC;AACvE,OAAO,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AAChE,OAAO,EACL,aAAa,GAEd,MAAM,+CAA+C,CAAC;AACvD,OAAO,EACL,YAAY,EACZ,SAAS,EACT,YAAY,GAGb,MAAM,8CAA8C,CAAC;AACtD,OAAO,EAAE,gBAAgB,EAAE,MAAM,8CAA8C,CAAC;AAChF,OAAO,EAAE,cAAc,EAAE,MAAM,+CAA+C,CAAC;AAC/E,OAAO,EAAE,KAAK,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAChF,OAAO,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAC;AACpD,OAAO,EACL,WAAW,EACX,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,iCAAiC,CAAC;AACzC,OAAO,EACL,MAAM,EACN,aAAa,EACb,iBAAiB,EACjB,YAAY,EACZ,WAAW,GACZ,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAE,QAAQ,EAAE,MAAM,8BAA8B,CAAC;AACxD,OAAO,EACL,KAAK,EACL,SAAS,EACT,SAAS,EACT,SAAS,EACT,WAAW,EACX,QAAQ,GACT,MAAM,2BAA2B,CAAC;AAEnC,MAAM,UAAU,IAAI;IAClB,OAAO,CAAC,EAAE,KAAK,EAAE,gCAAgC,EAAE,CAAC,CAAC;AACvD,CAAC;AAED,MAAM,WAAW,GAAG,EAAE,CAAC;AACvB,MAAM,cAAc,GAAG,EAAE,CAAC;AAS1B,sEAAsE;AACtE,SAAS,aAAa,CAAC,EACrB,KAAK,EACL,KAAK,EACL,OAAO,EACP,IAAI,EACJ,YAAY,GAOb;IACC,MAAM,CAAC,GAAG,IAAI,EAAE,CAAC;IACjB,OAAO,CACL,KAAC,MAAM,IAAC,IAAI,EAAE,IAAI,EAAE,YAAY,EAAE,YAAY,YAC5C,MAAC,aAAa,IAAC,SAAS,EAAC,WAAW,aAClC,MAAC,YAAY,eACX,KAAC,WAAW,cAAE,KAAK,CAAC,IAAI,GAAe,EACvC,KAAC,iBAAiB,cACf,CAAC,CAAC,gDAAgD,CAAC,GAClC,IACP,EAGf,KAAC,gBAAgB,IACf,KAAK,EAAE,KAAK,EACZ,OAAO,EAAE,OAAO,EAChB,EAAE,EAAE,KAAK,CAAC,EAAE,EACZ,IAAI,EAAE,KAAK,CAAC,IAAI,GAChB,IACY,GACT,CACV,CAAC;AACJ,CAAC;AAED,SAAS,cAAc,CAAC,EACtB,KAAK,EACL,OAAO,EACP,iBAAiB,EACjB,IAAI,EACJ,YAAY,GAOb;IACC,MAAM,CAAC,GAAG,IAAI,EAAE,CAAC;IACjB,MAAM,aAAa,GAAG,QAAQ,CAAC;QAC7B,QAAQ,EAAE,CAAC,qBAAqB,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,EAAE,cAAc,CAAC;QACpE,OAAO,EAAE,GAAG,EAAE,CACZ,aAAa,CACX,OAAO,EACP,qBAAqB,EACrB,EAAE,UAAU,EAAE,KAAK,CAAC,EAAE,EAAE,KAAK,EAAE,cAAc,EAAE,EAC/C,KAAK,CACN;QACH,OAAO,EAAE,IAAI,IAAI,CAAC,iBAAiB;KACpC,CAAC,CAAC;IAEH,OAAO,CACL,KAAC,MAAM,IAAC,IAAI,EAAE,IAAI,EAAE,YAAY,EAAE,YAAY,YAC5C,MAAC,aAAa,IAAC,SAAS,EAAC,WAAW,aAClC,MAAC,YAAY,eACX,KAAC,WAAW,cACT,CAAC,CAAC,2CAA2C,EAAE;gCAC9C,IAAI,EAAE,KAAK,CAAC,IAAI;6BACjB,CAAC,GACU,EACd,KAAC,iBAAiB,cACf,CAAC,CAAC,2CAA2C,CAAC,GAC7B,IACP,EACf,KAAC,aAAa,IACZ,MAAM,EACJ,iBAAiB;wBACf,CAAC,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,iBAAiB,EAAE;wBACjD,CAAC,CAAC,aAAa,CAAC,IAAI,EAExB,SAAS,EAAE,aAAa,CAAC,SAAS,EAClC,OAAO,EAAE,aAAa,CAAC,OAAO,EAC9B,KAAK,EAAE,aAAa,CAAC,KAAK,EAC1B,OAAO,EAAE,GAAG,EAAE,CAAC,KAAK,aAAa,CAAC,OAAO,EAAE,GAC3C,IACY,GACT,CACV,CAAC;AACJ,CAAC;AAED,SAAS,QAAQ,CAAC,EAChB,KAAK,EACL,KAAK,EACL,OAAO,EACP,UAAU,EACV,qBAAqB,EACrB,iBAAiB,GAQlB;IACC,MAAM,CAAC,GAAG,IAAI,EAAE,CAAC;IACjB,MAAM,QAAQ,GAAG,WAAW,EAAE,CAAC;IAC/B,MAAM,CAAC,WAAW,EAAE,cAAc,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IACtD,MAAM,CAAC,YAAY,EAAE,eAAe,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IACxD,MAAM,UAAU,GAAG,QAAQ,CAAC,QAAQ,CAAC,UAAU,CAAC,SAAS,CAAC;QACxD,CAAC,CAAC,4BAA4B;QAC9B,CAAC,CAAC,sBAAsB,CAAC;IAE3B,OAAO,CACL,MAAC,QAAQ,eACP,MAAC,SAAS,IAAC,SAAS,EAAC,WAAW,aAC9B,KAAC,IAAI,IACH,EAAE,EAAE,GAAG,UAAU,IAAI,KAAK,IAAI,KAAK,CAAC,EAAE,EAAE,EACxC,SAAS,EAAC,qDAAqD,YAE9D,KAAK,CAAC,IAAI,GACN,EACP,cAAK,SAAS,EAAC,yCAAyC,YACrD,KAAK,CAAC,EAAE,GACL,IACI,EACZ,KAAC,SAAS,IAAC,SAAS,EAAC,kDAAkD,YACpE,KAAK,CAAC,OAAO,GACJ,EACZ,KAAC,SAAS,IAAC,SAAS,EAAC,kDAAkD,YACpE,KAAK,CAAC,cAAc,GACX,EACZ,KAAC,SAAS,IAAC,SAAS,EAAC,kDAAkD,YACpE,KAAK,CAAC,WAAW,GACR,EACZ,KAAC,SAAS,IAAC,SAAS,EAAC,mBAAmB,YACtC,KAAC,SAAS,IAAC,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,GAAI,GAC3C,EACZ,KAAC,SAAS,IAAC,SAAS,EAAC,mBAAmB,YACtC,KAAC,YAAY,IACX,UAAU,EAAE,UAAU,EACtB,iBAAiB,EAAE,qBAAqB,EACxC,OAAO,EAAE,iBAAiB,GAC1B,GACQ,EACZ,KAAC,SAAS,IAAC,SAAS,EAAC,yCAAyC,YAC5D,KAAC,YAAY,IAAC,UAAU,EAAE,KAAK,CAAC,UAAU,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,GAAI,GACtD,EACZ,MAAC,SAAS,IAAC,SAAS,EAAC,WAAW,aAC9B,eAAK,SAAS,EAAC,wBAAwB,aACrC,MAAC,MAAM,IACL,OAAO,EAAC,SAAS,EACjB,IAAI,EAAC,IAAI,EACT,OAAO,EAAE,GAAG,EAAE,CAAC,cAAc,CAAC,IAAI,CAAC,aAEnC,KAAC,OAAO,IAAC,SAAS,EAAC,QAAQ,GAAG,EAC7B,CAAC,CAAC,qCAAqC,CAAC,IAClC,EACT,MAAC,MAAM,IACL,OAAO,EAAC,OAAO,EACf,IAAI,EAAC,IAAI,EACT,OAAO,EAAE,GAAG,EAAE,CAAC,eAAe,CAAC,IAAI,CAAC,aAEpC,KAAC,QAAQ,IAAC,SAAS,EAAC,QAAQ,GAAG,EAC9B,CAAC,CAAC,0CAA0C,CAAC,IACvC,IACL,EACN,KAAC,aAAa,IACZ,KAAK,EAAE,KAAK,EACZ,KAAK,EAAE,KAAK,EACZ,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,WAAW,EACjB,YAAY,EAAE,cAAc,GAC5B,EACF,KAAC,cAAc,IACb,KAAK,EAAE,KAAK,EACZ,OAAO,EAAE,OAAO,EAChB,iBAAiB,EAAE,qBAAqB,EACxC,IAAI,EAAE,YAAY,EAClB,YAAY,EAAE,eAAe,GAC7B,IACQ,IACH,CACZ,CAAC;AACJ,CAAC;AAED,SAAS,aAAa,CAAC,EACrB,OAAO,EACP,oBAAoB,EACpB,qBAAqB,EACrB,iBAAiB,GAMlB;IACC,MAAM,CAAC,GAAG,IAAI,EAAE,CAAC;IAEjB,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC;QAClB,OAAO,CACL,MAAC,KAAK,IAAC,OAAO,EAAC,aAAa,aAC1B,KAAC,iBAAiB,IAAC,SAAS,EAAC,QAAQ,GAAG,EACxC,KAAC,UAAU,cACR,CAAC,CAAC,+CAA+C,CAAC,GACxC,EACb,KAAC,gBAAgB,cAAE,OAAO,CAAC,KAAK,GAAoB,IAC9C,CACT,CAAC;IACJ,CAAC;IAED,OAAO,CACL,8BACG,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CACpB,MAAC,KAAK,IAAC,SAAS,EAAC,MAAM,aACrB,KAAC,cAAc,IAAC,SAAS,EAAC,QAAQ,GAAG,EACrC,KAAC,UAAU,cACR,CAAC,CAAC,8CAA8C,CAAC,GACvC,EACb,KAAC,gBAAgB,cAAE,OAAO,CAAC,UAAU,GAAoB,IACnD,CACT,CAAC,CAAC,CAAC,IAAI,EACP,OAAO,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CAC7B,cAAK,SAAS,EAAC,yEAAyE,YACrF,CAAC,CAAC,6CAA6C,CAAC,GAC7C,CACP,CAAC,CAAC,CAAC,CACF,MAAC,KAAK,eACJ,KAAC,WAAW,cACV,MAAC,QAAQ,eACP,KAAC,SAAS,cAAE,CAAC,CAAC,mCAAmC,CAAC,GAAa,EAC/D,KAAC,SAAS,cAAE,CAAC,CAAC,qCAAqC,CAAC,GAAa,EACjE,KAAC,SAAS,cACP,CAAC,CAAC,uCAAuC,CAAC,GACjC,EACZ,KAAC,SAAS,cAAE,CAAC,CAAC,oCAAoC,CAAC,GAAa,EAChE,KAAC,SAAS,cAAE,CAAC,CAAC,mCAAmC,CAAC,GAAa,EAC/D,KAAC,SAAS,cAAE,CAAC,CAAC,sCAAsC,CAAC,GAAa,EAClE,KAAC,SAAS,cAAE,CAAC,CAAC,sCAAsC,CAAC,GAAa,EAClE,KAAC,SAAS,KAAG,IACJ,GACC,EACd,KAAC,SAAS,cACP,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAC7B,KAAC,QAAQ,IAEP,KAAK,EAAE,KAAK,EACZ,KAAK,EAAE,OAAO,CAAC,KAAK,EACpB,OAAO,EAAE,OAAO,CAAC,OAAO,EACxB,UAAU,EAAE,oBAAoB,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,EAC9C,qBAAqB,EAAE,qBAAqB,EAC5C,iBAAiB,EAAE,iBAAiB,IAN/B,KAAK,CAAC,EAAE,CAOb,CACH,CAAC,GACQ,IACN,CACT,IACA,CACJ,CAAC;AACJ,CAAC;AAED,SAAS,YAAY,CAAC,EACpB,IAAI,EACJ,IAAI,EACJ,IAAI,EACJ,YAAY,EACZ,OAAO,EACP,SAAS,EACT,OAAO,EACP,KAAK,EACL,OAAO,EACP,oBAAoB,EACpB,qBAAqB,EACrB,iBAAiB,GAclB;IACC,OAAO,CACL,MAAC,WAAW,IACV,IAAI,EAAE,IAAI,EACV,YAAY,EAAE,YAAY,EAC1B,SAAS,EAAC,qBAAqB,aAE/B,MAAC,kBAAkB,IAAC,SAAS,EAAC,qGAAqG,aACjI,gBAAM,SAAS,EAAC,yBAAyB,aACvC,KAAC,gBAAgB,IAAC,SAAS,EAAC,+FAA+F,GAAG,EAC9H,eAAM,SAAS,EAAC,qCAAqC,YAAE,IAAI,GAAQ,IAC9D,EACP,eAAM,SAAS,EAAC,yCAAyC,YAAE,IAAI,GAAQ,IACpD,EACrB,KAAC,kBAAkB,IAAC,SAAS,EAAC,yBAAyB,YACpD,OAAO,CAAC,CAAC,CAAC,CACT,KAAC,gBAAgB,IAAC,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,GAAI,CACrD,CAAC,CAAC,CAAC,SAAS,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAC1B,KAAC,QAAQ,IAAC,SAAS,EAAC,aAAa,GAAG,CACrC,CAAC,CAAC,CAAC,CACF,KAAC,aAAa,IACZ,OAAO,EAAE,OAAO,EAChB,oBAAoB,EAAE,oBAAoB,EAC1C,qBAAqB,EAAE,qBAAqB,EAC5C,iBAAiB,EAAE,iBAAiB,GACpC,CACH,GACkB,IACT,CACf,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,OAAO,UAAU,uBAAuB;IAC7C,MAAM,CAAC,GAAG,IAAI,EAAE,CAAC;IACjB,MAAM,SAAS,GAAG,cAAc,CAAoB,qBAAqB,EAAE;QACzE,iBAAiB,EAAE,KAAK;KACzB,CAAC,CAAC;IAEH,MAAM,IAAI,GAAG,OAAO,CAClB,GAAG,EAAE,CACH,CAAC,SAAS,CAAC,IAAI,IAAI,EAAE,CAAC;SACnB,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,MAAM,KAAK,SAAS,CAAC;SACzC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,EACnE,CAAC,SAAS,CAAC,IAAI,CAAC,CACjB,CAAC;IAEF,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,QAAQ,CAA0B,EAAE,CAAC,CAAC;IACtE,MAAM,CAAC,UAAU,EAAE,aAAa,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;IAEnD,MAAM,WAAW,GAAG,cAAc,CAChC,2BAA2B,EAC3B,EAAE,UAAU,EAAE,WAAW,EAAE,CAC5B,CAAC;IAEF,MAAM,cAAc,GAAG,UAAU,CAAC;QAChC,OAAO,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;YAC1B,QAAQ,EAAE,CAAC,6BAA6B,EAAE,GAAG,CAAC,EAAE,EAAE,WAAW,CAAC;YAC9D,OAAO,EAAE,GAAG,EAAE,CAAC,oBAAoB,CAAC,GAAG,EAAE,WAAW,CAAC;YACrD,OAAO,EAAE,UAAU,IAAI,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,KAAK,IAAI;YAChD,SAAS,EAAE,QAAQ;SACpB,CAAC,CAAC;KACJ,CAAC,CAAC;IAEH,MAAM,aAAa,GAAG,OAAO,CAA8B,GAAG,EAAE;QAC9D,IAAI,CAAC,WAAW,CAAC,IAAI;YAAE,OAAO,SAAS,CAAC;QACxC,MAAM,SAAS,GAAG,qBAAqB,CACrC,WAAW,CAAC,IAAI,EAChB,cAAc;aACX,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC;aAC1B,MAAM,CAAC,CAAC,OAAO,EAA8B,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CACrE,CAAC;QACF,OAAO;YACL,KAAK,EAAE,MAAM;YACb,OAAO,EAAE,CAAC,CAAC,yCAAyC,CAAC;YACrD,OAAO,EAAE,CAAC,CAAC,4CAA4C,CAAC;YACxD,MAAM,EAAE,SAAS,CAAC,MAAM;YACxB,KAAK,EAAE,IAAI;YACX,UAAU,EAAE,SAAS,CAAC,UAAU;SACjC,CAAC;IACJ,CAAC,EAAE,CAAC,WAAW,CAAC,IAAI,EAAE,cAAc,EAAE,CAAC,CAAC,CAAC,CAAC;IAE1C,MAAM,iBAAiB,GAAG,UAAU,CAAC;QACnC,OAAO,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE;YAC/B,MAAM,OAAO,GAAG,cAAc,CAAC,KAAK,CAAC,EAAE,IAAI,CAAC;YAC5C,MAAM,WAAW,GACf,OAAO,IAAI,CAAC,OAAO,CAAC,KAAK;gBACvB,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC;gBACzC,CAAC,CAAC,EAAE,CAAC;YACT,OAAO;gBACL,QAAQ,EAAE;oBACR,uBAAuB;oBACvB,GAAG,CAAC,EAAE;oBACN,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC;oBACrB,WAAW;iBACZ;gBACD,OAAO,EAAE,GAAG,EAAE,CACZ,aAAa,CACX,GAAG,CAAC,IAAI,EACR,uBAAuB,EACvB,EAAE,WAAW,EAAE,UAAU,EAAE,WAAW,EAAE,EACxC,MAAM,CACP;gBACH,OAAO,EAAE,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,KAAK,IAAI,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC;aAC7D,CAAC;QACJ,CAAC,CAAC;KACH,CAAC,CAAC;IAEH,MAAM,aAAa,GACjB,WAAW,CAAC,SAAS;QACrB,CAAC,UAAU,IAAI,cAAc,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC;IAClE,MAAM,WAAW,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,KAAK,CAAC;IACzE,MAAM,oBAAoB,GAAG,CAAC,CAC5B,8DAA8D,CAC/D,CAAC;IAEF,OAAO,CACL,KAAC,aAAa,IACZ,KAAK,EAAE,CAAC,CAAC,mCAAmC,CAAC,EAC7C,WAAW,EAAE,CAAC,CAAC,yCAAyC,CAAC,YAEzD,eAAK,SAAS,EAAC,qBAAqB,aAClC,MAAC,KAAK,eACJ,KAAC,cAAc,IAAC,SAAS,EAAC,QAAQ,GAAG,EACrC,KAAC,UAAU,cACR,CAAC,CAAC,4CAA4C,CAAC,GACrC,EACb,KAAC,gBAAgB,cACd,CAAC,CAAC,2CAA2C,CAAC,GAC9B,IACb,EAEP,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,CACnB,KAAC,gBAAgB,IACf,KAAK,EAAE,SAAS,CAAC,KAAK,EACtB,OAAO,EAAE,GAAG,EAAE,CAAC,KAAK,SAAS,CAAC,OAAO,EAAE,GACvC,CACH,CAAC,CAAC,CAAC,IAAI,EAEP,SAAS,CAAC,SAAS,CAAC,CAAC,CAAC,CACrB,eAAK,SAAS,EAAC,WAAW,aACxB,KAAC,QAAQ,IAAC,SAAS,EAAC,yBAAyB,GAAG,EAChD,KAAC,QAAQ,IAAC,SAAS,EAAC,yBAAyB,GAAG,IAC5C,CACP,CAAC,CAAC,CAAC,IAAI,EAEP,CAAC,SAAS,CAAC,SAAS,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CAC3C,eAAK,SAAS,EAAC,uFAAuF,aACpG,KAAC,QAAQ,IAAC,SAAS,EAAC,qBAAqB,GAAG,EAC3C,CAAC,CAAC,oCAAoC,CAAC,IACpC,CACP,CAAC,CAAC,CAAC,IAAI,EAEP,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CACjB,MAAC,WAAW,IAAC,WAAW,QAAC,SAAS,EAAC,qBAAqB,aACtD,KAAC,kBAAkB,IAAC,SAAS,EAAC,qGAAqG,YACjI,gBAAM,SAAS,EAAC,yBAAyB,aACvC,KAAC,gBAAgB,IAAC,SAAS,EAAC,+FAA+F,GAAG,EAC9H,eAAM,SAAS,EAAC,qCAAqC,YAClD,CAAC,CAAC,0CAA0C,CAAC,GACzC,IACF,GACY,EACrB,KAAC,kBAAkB,IAAC,SAAS,EAAC,yBAAyB,YACrD,KAAC,cAAc,IAAC,IAAI,EAAE,IAAI,GAAI,GACX,IACT,CACf,CAAC,CAAC,CAAC,IAAI,EAER,KAAC,YAAY,IACX,IAAI,EAAE,CAAC,CAAC,yCAAyC,CAAC,EAClD,IAAI,EAAE,CAAC,CAAC,4CAA4C,CAAC,EACrD,IAAI,EAAE,UAAU,EAChB,YAAY,EAAE,aAAa,EAC3B,OAAO,EAAE,aAAa,EACtB,SAAS,EAAE,aAAa,EACxB,OAAO,EAAE,WAAW,CAAC,OAAO,IAAI,OAAO,CAAC,WAAW,CAAC,EACpD,KAAK,EAAE,WAAW,CAAC,KAAK,IAAI,WAAW,EACvC,OAAO,EAAE,GAAG,EAAE;wBACZ,KAAK,WAAW,CAAC,OAAO,EAAE,CAAC;wBAC3B,KAAK,MAAM,KAAK,IAAI,cAAc;4BAAE,KAAK,KAAK,CAAC,OAAO,EAAE,CAAC;oBAC3D,CAAC,EACD,oBAAoB,EAAE,IAAI,GAAG,EAAE,EAC/B,qBAAqB,EAAE,oBAAoB,EAC3C,iBAAiB,EAAE,KAAK,GACxB,EAED,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE;oBACvB,MAAM,KAAK,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC;oBACpC,MAAM,eAAe,GAAG,iBAAiB,CAAC,KAAK,CAAC,CAAC;oBACjD,MAAM,gBAAgB,GAAG,eAAe,EAAE,IAAI,CAAC;oBAC/C,MAAM,qBAAqB,GACzB,gBAAgB,IAAI,CAAC,gBAAgB,CAAC,SAAS;wBAC7C,CAAC,CAAC,gBAAgB,CAAC,MAAM;wBACzB,CAAC,CAAC,eAAe,EAAE,OAAO;4BACxB,CAAC,CAAC,eAAe,CAAC,KAAK,YAAY,KAAK;gCACtC,CAAC,CAAC,eAAe,CAAC,KAAK,CAAC,OAAO;gCAC/B,CAAC,CAAC,MAAM,CAAC,eAAe,CAAC,KAAK,CAAC;4BACjC,CAAC,CAAC,IAAI,CAAC;oBACb,MAAM,oBAAoB,GAAG,IAAI,GAAG,EAA2B,CAAC;oBAChE,IAAI,gBAAgB,EAAE,SAAS,EAAE,CAAC;wBAChC,KAAK,MAAM,KAAK,IAAI,gBAAgB,CAAC,IAAI,EAAE,CAAC;4BAC1C,oBAAoB,CAAC,GAAG,CAAC,KAAK,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;wBACpD,CAAC;oBACH,CAAC;oBACD,OAAO,CACL,KAAC,YAAY,IAEX,IAAI,EAAE,GAAG,CAAC,IAAI,EACd,IAAI,EAAE,GAAG,CAAC,IAAI,EACd,IAAI,EAAE,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,KAAK,IAAI,EAC/B,YAAY,EAAE,CAAC,IAAI,EAAE,EAAE,CACrB,WAAW,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,OAAO,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,EAE5D,OAAO,EAAE,KAAK,EAAE,IAAI,EACpB,SAAS,EAAE,KAAK,EAAE,SAAS,IAAI,KAAK,EACpC,OAAO,EAAE,KAAK,EAAE,OAAO,IAAI,KAAK,EAChC,KAAK,EAAE,KAAK,EAAE,KAAK,EACnB,OAAO,EAAE,GAAG,EAAE,CAAC,KAAK,KAAK,EAAE,OAAO,EAAE,EACpC,oBAAoB,EAAE,oBAAoB,EAC1C,qBAAqB,EAAE,qBAAqB,EAC5C,iBAAiB,EAAE,eAAe,EAAE,SAAS,IAAI,KAAK,IAdjD,GAAG,CAAC,EAAE,CAeX,CACH,CAAC;gBACJ,CAAC,CAAC,IACE,GACQ,CACjB,CAAC;AACJ,CAAC","sourcesContent":["import { useActionQuery } from \"@agent-native/core/client/hooks\";\nimport { useT } from \"@agent-native/core/client/i18n\";\nimport {\n IconAlertTriangle,\n IconChevronRight,\n IconEye,\n IconInfoCircle,\n IconList,\n IconMail,\n} from \"@tabler/icons-react\";\nimport { useQueries, useQuery } from \"@tanstack/react-query\";\nimport { useMemo, useState } from \"react\";\nimport { Link, useLocation } from \"react-router\";\n\nimport {\n aggregateSharedEmails,\n callAppAction,\n fetchAppEmailCatalog,\n type AppEmailCatalog,\n type AppTransactionalEmail,\n type LocalTransactionalEmailCatalog,\n} from \"../../client/transactional-emails\";\nimport { ActionQueryError } from \"../../components/action-query-error\";\nimport { DispatchShell } from \"../../components/dispatch-shell\";\nimport {\n ActivityTable,\n type EmailActivityEntry,\n} from \"../../components/transactional-email-activity\";\nimport {\n OpenRateCell,\n SendsCell,\n LastSentCell,\n type EmailEngagement,\n type ProviderMetricsResult,\n} from \"../../components/transactional-email-metrics\";\nimport { EmailPreviewPane } from \"../../components/transactional-email-preview\";\nimport { SendLogSection } from \"../../components/transactional-email-send-log\";\nimport { Alert, AlertDescription, AlertTitle } from \"../../components/ui/alert\";\nimport { Button } from \"../../components/ui/button\";\nimport {\n Collapsible,\n CollapsibleContent,\n CollapsibleTrigger,\n} from \"../../components/ui/collapsible\";\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogHeader,\n DialogTitle,\n} from \"../../components/ui/dialog\";\nimport { Skeleton } from \"../../components/ui/skeleton\";\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from \"../../components/ui/table\";\n\nexport function meta() {\n return [{ title: \"Transactional email — Dispatch\" }];\n}\n\nconst WINDOW_DAYS = 30;\nconst ACTIVITY_LIMIT = 50;\n\ninterface WorkspaceAppRef {\n id: string;\n name: string;\n path: string;\n status?: \"ready\" | \"pending\";\n}\n\n/** Shape of the local `list-transactional-emails` action response. */\nfunction PreviewDialog({\n email,\n appId,\n appPath,\n open,\n onOpenChange,\n}: {\n email: AppTransactionalEmail;\n appId: string;\n appPath: string;\n open: boolean;\n onOpenChange: (next: boolean) => void;\n}) {\n const t = useT();\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent className=\"max-w-3xl\">\n <DialogHeader>\n <DialogTitle>{email.name}</DialogTitle>\n <DialogDescription>\n {t(\"dispatch.transactionalEmail.previewDescription\")}\n </DialogDescription>\n </DialogHeader>\n {/* Dialog content only mounts while open, so this fetches on open and\n drops the request when closed rather than needing an enabled flag. */}\n <EmailPreviewPane\n appId={appId}\n appPath={appPath}\n id={email.id}\n name={email.name}\n />\n </DialogContent>\n </Dialog>\n );\n}\n\nfunction ActivityDialog({\n email,\n appPath,\n unavailableReason,\n open,\n onOpenChange,\n}: {\n email: AppTransactionalEmail;\n appPath: string;\n unavailableReason: string | null;\n open: boolean;\n onOpenChange: (next: boolean) => void;\n}) {\n const t = useT();\n const activityQuery = useQuery({\n queryKey: [\"list-email-activity\", appPath, email.id, ACTIVITY_LIMIT],\n queryFn: () =>\n callAppAction<ProviderMetricsResult<EmailActivityEntry[]>>(\n appPath,\n \"list-email-activity\",\n { templateId: email.id, limit: ACTIVITY_LIMIT },\n \"GET\",\n ),\n enabled: open && !unavailableReason,\n });\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent className=\"max-w-4xl\">\n <DialogHeader>\n <DialogTitle>\n {t(\"dispatch.transactionalEmail.activityTitle\", {\n name: email.name,\n })}\n </DialogTitle>\n <DialogDescription>\n {t(\"dispatch.transactionalEmail.retentionNote\")}\n </DialogDescription>\n </DialogHeader>\n <ActivityTable\n result={\n unavailableReason\n ? { available: false, reason: unavailableReason }\n : activityQuery.data\n }\n isLoading={activityQuery.isLoading}\n isError={activityQuery.isError}\n error={activityQuery.error}\n onRetry={() => void activityQuery.refetch()}\n />\n </DialogContent>\n </Dialog>\n );\n}\n\nfunction EmailRow({\n email,\n appId,\n appPath,\n engagement,\n engagementUnavailable,\n engagementLoading,\n}: {\n email: AppTransactionalEmail;\n appId: string;\n appPath: string;\n engagement: EmailEngagement | undefined;\n engagementUnavailable: string | null;\n engagementLoading: boolean;\n}) {\n const t = useT();\n const location = useLocation();\n const [previewOpen, setPreviewOpen] = useState(false);\n const [activityOpen, setActivityOpen] = useState(false);\n const detailBase = location.pathname.startsWith(\"/admin/\")\n ? \"/admin/transactional-email\"\n : \"/transactional-email\";\n\n return (\n <TableRow>\n <TableCell className=\"align-top\">\n <Link\n to={`${detailBase}/${appId}/${email.id}`}\n className=\"text-sm font-medium text-foreground hover:underline\"\n >\n {email.name}\n </Link>\n <div className=\"font-mono text-xs text-muted-foreground\">\n {email.id}\n </div>\n </TableCell>\n <TableCell className=\"max-w-64 align-top text-xs text-muted-foreground\">\n {email.trigger}\n </TableCell>\n <TableCell className=\"max-w-56 align-top text-xs text-muted-foreground\">\n {email.recipientLabel}\n </TableCell>\n <TableCell className=\"max-w-56 align-top text-xs text-muted-foreground\">\n {email.senderLabel}\n </TableCell>\n <TableCell className=\"align-top text-sm\">\n <SendsCell sent={email.sent} failed={email.failed} />\n </TableCell>\n <TableCell className=\"align-top text-sm\">\n <OpenRateCell\n engagement={engagement}\n unavailableReason={engagementUnavailable}\n loading={engagementLoading}\n />\n </TableCell>\n <TableCell className=\"align-top text-xs text-muted-foreground\">\n <LastSentCell lastSentAt={email.lastSentAt} sent={email.sent} />\n </TableCell>\n <TableCell className=\"align-top\">\n <div className=\"flex justify-end gap-2\">\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => setPreviewOpen(true)}\n >\n <IconEye className=\"size-4\" />\n {t(\"dispatch.transactionalEmail.preview\")}\n </Button>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => setActivityOpen(true)}\n >\n <IconList className=\"size-4\" />\n {t(\"dispatch.transactionalEmail.activityLink\")}\n </Button>\n </div>\n <PreviewDialog\n email={email}\n appId={appId}\n appPath={appPath}\n open={previewOpen}\n onOpenChange={setPreviewOpen}\n />\n <ActivityDialog\n email={email}\n appPath={appPath}\n unavailableReason={engagementUnavailable}\n open={activityOpen}\n onOpenChange={setActivityOpen}\n />\n </TableCell>\n </TableRow>\n );\n}\n\nfunction AppEmailTable({\n catalog,\n engagementByTemplate,\n engagementUnavailable,\n engagementLoading,\n}: {\n catalog: AppEmailCatalog;\n engagementByTemplate: Map<string, EmailEngagement>;\n engagementUnavailable: string | null;\n engagementLoading: boolean;\n}) {\n const t = useT();\n\n if (catalog.error) {\n return (\n <Alert variant=\"destructive\">\n <IconAlertTriangle className=\"size-4\" />\n <AlertTitle>\n {t(\"dispatch.transactionalEmail.catalogUnreadable\")}\n </AlertTitle>\n <AlertDescription>{catalog.error}</AlertDescription>\n </Alert>\n );\n }\n\n return (\n <>\n {catalog.statsError ? (\n <Alert className=\"mb-4\">\n <IconInfoCircle className=\"size-4\" />\n <AlertTitle>\n {t(\"dispatch.transactionalEmail.countsUnreadable\")}\n </AlertTitle>\n <AlertDescription>{catalog.statsError}</AlertDescription>\n </Alert>\n ) : null}\n {catalog.emails.length === 0 ? (\n <div className=\"rounded-xl border border-dashed px-4 py-6 text-sm text-muted-foreground\">\n {t(\"dispatch.transactionalEmail.appSendsNoEmail\")}\n </div>\n ) : (\n <Table>\n <TableHeader>\n <TableRow>\n <TableHead>{t(\"dispatch.transactionalEmail.email\")}</TableHead>\n <TableHead>{t(\"dispatch.transactionalEmail.trigger\")}</TableHead>\n <TableHead>\n {t(\"dispatch.transactionalEmail.recipient\")}\n </TableHead>\n <TableHead>{t(\"dispatch.transactionalEmail.sender\")}</TableHead>\n <TableHead>{t(\"dispatch.transactionalEmail.sends\")}</TableHead>\n <TableHead>{t(\"dispatch.transactionalEmail.openRate\")}</TableHead>\n <TableHead>{t(\"dispatch.transactionalEmail.lastSent\")}</TableHead>\n <TableHead />\n </TableRow>\n </TableHeader>\n <TableBody>\n {catalog.emails.map((email) => (\n <EmailRow\n key={email.id}\n email={email}\n appId={catalog.appId}\n appPath={catalog.appPath}\n engagement={engagementByTemplate.get(email.id)}\n engagementUnavailable={engagementUnavailable}\n engagementLoading={engagementLoading}\n />\n ))}\n </TableBody>\n </Table>\n )}\n </>\n );\n}\n\nfunction AppEmailCard({\n name,\n path,\n open,\n onOpenChange,\n catalog,\n isLoading,\n isError,\n error,\n onRetry,\n engagementByTemplate,\n engagementUnavailable,\n engagementLoading,\n}: {\n name: string;\n path: string;\n open: boolean;\n onOpenChange: (next: boolean) => void;\n catalog: AppEmailCatalog | undefined;\n isLoading: boolean;\n isError: boolean;\n error: unknown;\n onRetry: () => void;\n engagementByTemplate: Map<string, EmailEngagement>;\n engagementUnavailable: string | null;\n engagementLoading: boolean;\n}) {\n return (\n <Collapsible\n open={open}\n onOpenChange={onOpenChange}\n className=\"rounded-2xl bg-card\"\n >\n <CollapsibleTrigger className=\"group flex w-full cursor-pointer items-center justify-between gap-3 p-5 text-left hover:bg-muted/20\">\n <span className=\"flex items-center gap-2\">\n <IconChevronRight className=\"h-4 w-4 shrink-0 text-muted-foreground transition-transform group-data-[state=open]:rotate-90\" />\n <span className=\"text-sm font-medium text-foreground\">{name}</span>\n </span>\n <span className=\"font-mono text-xs text-muted-foreground\">{path}</span>\n </CollapsibleTrigger>\n <CollapsibleContent className=\"border-t px-5 pb-5 pt-4\">\n {isError ? (\n <ActionQueryError error={error} onRetry={onRetry} />\n ) : isLoading || !catalog ? (\n <Skeleton className=\"h-24 w-full\" />\n ) : (\n <AppEmailTable\n catalog={catalog}\n engagementByTemplate={engagementByTemplate}\n engagementUnavailable={engagementUnavailable}\n engagementLoading={engagementLoading}\n />\n )}\n </CollapsibleContent>\n </Collapsible>\n );\n}\n\nexport default function TransactionalEmailRoute() {\n const t = useT();\n const appsQuery = useActionQuery<WorkspaceAppRef[]>(\"list-workspace-apps\", {\n includeAgentCards: false,\n });\n\n const apps = useMemo(\n () =>\n (appsQuery.data ?? [])\n .filter((app) => app.status !== \"pending\")\n .map((app) => ({ id: app.id, name: app.name, path: app.path })),\n [appsQuery.data],\n );\n\n const [expanded, setExpanded] = useState<Record<string, boolean>>({});\n const [sharedOpen, setSharedOpen] = useState(true);\n\n const sharedQuery = useActionQuery<LocalTransactionalEmailCatalog>(\n \"list-transactional-emails\",\n { windowDays: WINDOW_DAYS },\n );\n\n const catalogQueries = useQueries({\n queries: apps.map((app) => ({\n queryKey: [\"transactional-email-catalog\", app.id, WINDOW_DAYS],\n queryFn: () => fetchAppEmailCatalog(app, WINDOW_DAYS),\n enabled: sharedOpen || expanded[app.id] === true,\n staleTime: Infinity,\n })),\n });\n\n const sharedCatalog = useMemo<AppEmailCatalog | undefined>(() => {\n if (!sharedQuery.data) return undefined;\n const aggregate = aggregateSharedEmails(\n sharedQuery.data,\n catalogQueries\n .map((query) => query.data)\n .filter((catalog): catalog is AppEmailCatalog => Boolean(catalog)),\n );\n return {\n appId: \"core\",\n appName: t(\"dispatch.transactionalEmail.sharedTitle\"),\n appPath: t(\"dispatch.transactionalEmail.sharedSubtitle\"),\n emails: aggregate.emails,\n error: null,\n statsError: aggregate.statsError,\n };\n }, [sharedQuery.data, catalogQueries, t]);\n\n const engagementQueries = useQueries({\n queries: apps.map((app, index) => {\n const catalog = catalogQueries[index]?.data;\n const templateIds =\n catalog && !catalog.error\n ? catalog.emails.map((email) => email.id)\n : [];\n return {\n queryKey: [\n \"list-email-engagement\",\n app.id,\n templateIds.join(\",\"),\n WINDOW_DAYS,\n ],\n queryFn: () =>\n callAppAction<ProviderMetricsResult<EmailEngagement[]>>(\n app.path,\n \"list-email-engagement\",\n { templateIds, windowDays: WINDOW_DAYS },\n \"POST\",\n ),\n enabled: expanded[app.id] === true && templateIds.length > 0,\n };\n }),\n });\n\n const sharedLoading =\n sharedQuery.isLoading ||\n (sharedOpen && catalogQueries.some((query) => query.isLoading));\n const sharedError = catalogQueries.find((query) => query.isError)?.error;\n const sharedProviderReason = t(\n \"dispatch.transactionalEmail.sharedProviderMetricsUnavailable\",\n );\n\n return (\n <DispatchShell\n title={t(\"dispatch.transactionalEmail.title\")}\n description={t(\"dispatch.transactionalEmail.description\")}\n >\n <div className=\"flex flex-col gap-4\">\n <Alert>\n <IconInfoCircle className=\"size-4\" />\n <AlertTitle>\n {t(\"dispatch.transactionalEmail.retentionTitle\")}\n </AlertTitle>\n <AlertDescription>\n {t(\"dispatch.transactionalEmail.retentionNote\")}\n </AlertDescription>\n </Alert>\n\n {appsQuery.isError ? (\n <ActionQueryError\n error={appsQuery.error}\n onRetry={() => void appsQuery.refetch()}\n />\n ) : null}\n\n {appsQuery.isLoading ? (\n <div className=\"space-y-3\">\n <Skeleton className=\"h-16 w-full rounded-2xl\" />\n <Skeleton className=\"h-16 w-full rounded-2xl\" />\n </div>\n ) : null}\n\n {!appsQuery.isLoading && apps.length === 0 ? (\n <div className=\"rounded-2xl border border-dashed px-6 py-12 text-center text-sm text-muted-foreground\">\n <IconMail className=\"mx-auto mb-2 size-5\" />\n {t(\"dispatch.transactionalEmail.noApps\")}\n </div>\n ) : null}\n\n {apps.length > 0 ? (\n <Collapsible defaultOpen className=\"rounded-2xl bg-card\">\n <CollapsibleTrigger className=\"group flex w-full cursor-pointer items-center justify-between gap-3 p-5 text-left hover:bg-muted/20\">\n <span className=\"flex items-center gap-2\">\n <IconChevronRight className=\"h-4 w-4 shrink-0 text-muted-foreground transition-transform group-data-[state=open]:rotate-90\" />\n <span className=\"text-sm font-medium text-foreground\">\n {t(\"dispatch.transactionalEmail.sendLogTitle\")}\n </span>\n </span>\n </CollapsibleTrigger>\n <CollapsibleContent className=\"border-t px-5 pb-5 pt-4\">\n <SendLogSection apps={apps} />\n </CollapsibleContent>\n </Collapsible>\n ) : null}\n\n <AppEmailCard\n name={t(\"dispatch.transactionalEmail.sharedTitle\")}\n path={t(\"dispatch.transactionalEmail.sharedSubtitle\")}\n open={sharedOpen}\n onOpenChange={setSharedOpen}\n catalog={sharedCatalog}\n isLoading={sharedLoading}\n isError={sharedQuery.isError || Boolean(sharedError)}\n error={sharedQuery.error ?? sharedError}\n onRetry={() => {\n void sharedQuery.refetch();\n for (const query of catalogQueries) void query.refetch();\n }}\n engagementByTemplate={new Map()}\n engagementUnavailable={sharedProviderReason}\n engagementLoading={false}\n />\n\n {apps.map((app, index) => {\n const query = catalogQueries[index];\n const engagementQuery = engagementQueries[index];\n const engagementResult = engagementQuery?.data;\n const engagementUnavailable =\n engagementResult && !engagementResult.available\n ? engagementResult.reason\n : engagementQuery?.isError\n ? engagementQuery.error instanceof Error\n ? engagementQuery.error.message\n : String(engagementQuery.error)\n : null;\n const engagementByTemplate = new Map<string, EmailEngagement>();\n if (engagementResult?.available) {\n for (const entry of engagementResult.data) {\n engagementByTemplate.set(entry.templateId, entry);\n }\n }\n return (\n <AppEmailCard\n key={app.id}\n name={app.name}\n path={app.path}\n open={expanded[app.id] === true}\n onOpenChange={(next) =>\n setExpanded((current) => ({ ...current, [app.id]: next }))\n }\n catalog={query?.data}\n isLoading={query?.isLoading ?? false}\n isError={query?.isError ?? false}\n error={query?.error}\n onRetry={() => void query?.refetch()}\n engagementByTemplate={engagementByTemplate}\n engagementUnavailable={engagementUnavailable}\n engagementLoading={engagementQuery?.isLoading ?? false}\n />\n );\n })}\n </div>\n </DispatchShell>\n );\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/dispatch",
3
- "version": "0.34.0",
3
+ "version": "0.35.0-nightly-20260908153602",
4
4
  "description": "Dispatch — workspace control plane for agent-native apps. Vault, integrations, destinations, scheduled jobs, and cross-app delegation, shipped as a single drop-in package.",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -102,10 +102,10 @@
102
102
  "typescript-7": "npm:typescript@^7.0.2",
103
103
  "vite": "8.1.0",
104
104
  "vitest": "^4.1.5",
105
- "@agent-native/core": "0.177.0"
105
+ "@agent-native/core": "0.177.1-nightly-20260908153602"
106
106
  },
107
107
  "peerDependencies": {
108
- "@agent-native/core": ">=0.8.0",
108
+ "@agent-native/core": "0.177.1-nightly-20260908153602",
109
109
  "react": ">=19.2.7",
110
110
  "react-dom": ">=19.2.7",
111
111
  "react-router": ">=8"
@@ -117,7 +117,11 @@ export async function callAppAction<T>(
117
117
  },
118
118
  ...(method === "POST" ? { body: JSON.stringify(params) } : {}),
119
119
  });
120
- if (!res.ok) throw new Error(`${action} failed: HTTP ${res.status}`);
120
+ if (!res.ok) {
121
+ const error = new Error(`${action} failed: HTTP ${res.status}`);
122
+ (error as Error & { status: number }).status = res.status;
123
+ throw error;
124
+ }
121
125
  return (await res.json()) as T;
122
126
  }
123
127
 
@@ -0,0 +1,335 @@
1
+ import { useT } from "@agent-native/core/client/i18n";
2
+ import {
3
+ DateRangePicker,
4
+ dateRangeToInterval,
5
+ type DateRange,
6
+ } from "@agent-native/toolkit/dashboard";
7
+ import { useQuery } from "@tanstack/react-query";
8
+ import { useEffect, useState } from "react";
9
+
10
+ import { callAppAction } from "../client/transactional-emails";
11
+ import { ActionQueryError } from "./action-query-error";
12
+ import { Badge } from "./ui/badge";
13
+ import { Button } from "./ui/button";
14
+ import { Dialog, DialogContent, DialogHeader, DialogTitle } from "./ui/dialog";
15
+ import { Input } from "./ui/input";
16
+ import {
17
+ Select,
18
+ SelectContent,
19
+ SelectItem,
20
+ SelectTrigger,
21
+ SelectValue,
22
+ } from "./ui/select";
23
+ import { Skeleton } from "./ui/skeleton";
24
+ import {
25
+ Table,
26
+ TableBody,
27
+ TableCell,
28
+ TableHead,
29
+ TableHeader,
30
+ TableRow,
31
+ } from "./ui/table";
32
+
33
+ interface SendLogEntry {
34
+ id: string;
35
+ templateId: string | null;
36
+ app: string | null;
37
+ recipient: string;
38
+ sender: string;
39
+ subject: string;
40
+ status: string;
41
+ error: string | null;
42
+ provider: string;
43
+ responseStatus: number | null;
44
+ createdAt: number;
45
+ }
46
+
47
+ /** One page of `list-email-log`, fetched at `PAGE_SIZE + 1` to detect "has more". */
48
+ const PAGE_SIZE = 50;
49
+
50
+ function useDebounced(value: string, delayMs = 300): string {
51
+ const [debounced, setDebounced] = useState(value);
52
+ useEffect(() => {
53
+ const timer = setTimeout(() => setDebounced(value), delayMs);
54
+ return () => clearTimeout(timer);
55
+ }, [value, delayMs]);
56
+ return debounced;
57
+ }
58
+
59
+ function SendLogDetailDialog({
60
+ entry,
61
+ open,
62
+ onOpenChange,
63
+ }: {
64
+ entry: SendLogEntry | null;
65
+ open: boolean;
66
+ onOpenChange: (next: boolean) => void;
67
+ }) {
68
+ const t = useT();
69
+ if (!entry) return null;
70
+
71
+ return (
72
+ <Dialog open={open} onOpenChange={onOpenChange}>
73
+ <DialogContent className="max-w-3xl">
74
+ <DialogHeader>
75
+ <DialogTitle>{entry.subject}</DialogTitle>
76
+ </DialogHeader>
77
+ <div className="grid grid-cols-2 gap-x-6 gap-y-1 text-xs">
78
+ <div className="text-muted-foreground">
79
+ {t("dispatch.transactionalEmail.recipient")}
80
+ </div>
81
+ <div className="font-mono">{entry.recipient}</div>
82
+ <div className="text-muted-foreground">
83
+ {t("dispatch.transactionalEmail.sender")}
84
+ </div>
85
+ <div className="font-mono">{entry.sender}</div>
86
+ <div className="text-muted-foreground">
87
+ {t("dispatch.transactionalEmail.sendLogTemplate")}
88
+ </div>
89
+ <div className="font-mono">{entry.templateId ?? "—"}</div>
90
+ <div className="text-muted-foreground">
91
+ {t("dispatch.transactionalEmail.sendLogProvider")}
92
+ </div>
93
+ <div>{entry.provider}</div>
94
+ <div className="text-muted-foreground">
95
+ {t("dispatch.transactionalEmail.sendLogResponseStatus")}
96
+ </div>
97
+ <div>{entry.responseStatus ?? "—"}</div>
98
+ {entry.error ? (
99
+ <>
100
+ <div className="text-muted-foreground">
101
+ {t("dispatch.transactionalEmail.sendLogError")}
102
+ </div>
103
+ <div className="text-destructive">{entry.error}</div>
104
+ </>
105
+ ) : null}
106
+ </div>
107
+ </DialogContent>
108
+ </Dialog>
109
+ );
110
+ }
111
+
112
+ export function SendLogSection({
113
+ apps,
114
+ }: {
115
+ apps: { id: string; name: string; path: string }[];
116
+ }) {
117
+ const t = useT();
118
+ const [appId, setAppId] = useState<string | undefined>(apps[0]?.id);
119
+ const [dateRange, setDateRange] = useState<DateRange>("7d");
120
+ const [to, setTo] = useState("");
121
+ const [from, setFrom] = useState("");
122
+ const [status, setStatus] = useState<string>("all");
123
+ const [provider, setProvider] = useState<string>("all");
124
+ const [offset, setOffset] = useState(0);
125
+ const [selected, setSelected] = useState<SendLogEntry | null>(null);
126
+
127
+ const debouncedTo = useDebounced(to);
128
+ const debouncedFrom = useDebounced(from);
129
+ const selectedApp = apps.find((app) => app.id === appId) ?? apps[0];
130
+
131
+ useEffect(() => {
132
+ setOffset(0);
133
+ }, [appId, dateRange, debouncedTo, debouncedFrom, status, provider]);
134
+
135
+ const query = useQuery({
136
+ queryKey: [
137
+ "list-email-log",
138
+ selectedApp?.path,
139
+ dateRange,
140
+ debouncedTo,
141
+ debouncedFrom,
142
+ status,
143
+ provider,
144
+ offset,
145
+ ],
146
+ queryFn: () =>
147
+ callAppAction<{ entries: SendLogEntry[] }>(
148
+ selectedApp!.path,
149
+ "list-email-log",
150
+ {
151
+ sinceMs: Date.now() - dateRangeToInterval(dateRange) * 86_400_000,
152
+ ...(debouncedTo ? { to: debouncedTo } : {}),
153
+ ...(debouncedFrom ? { from: debouncedFrom } : {}),
154
+ ...(status !== "all" ? { status } : {}),
155
+ ...(provider !== "all" ? { provider } : {}),
156
+ limit: PAGE_SIZE + 1,
157
+ offset,
158
+ },
159
+ "GET",
160
+ ),
161
+ enabled: Boolean(selectedApp),
162
+ });
163
+
164
+ const entries = (query.data?.entries ?? []).slice(0, PAGE_SIZE);
165
+ const hasMore = (query.data?.entries.length ?? 0) > PAGE_SIZE;
166
+
167
+ if (apps.length === 0) return null;
168
+
169
+ return (
170
+ <div>
171
+ <div className="mb-4 flex flex-wrap items-center gap-2">
172
+ <Select value={selectedApp?.id} onValueChange={setAppId}>
173
+ <SelectTrigger className="w-48">
174
+ <SelectValue placeholder={t("dispatch.transactionalEmail.app")} />
175
+ </SelectTrigger>
176
+ <SelectContent>
177
+ {apps.map((app) => (
178
+ <SelectItem key={app.id} value={app.id}>
179
+ {app.name}
180
+ </SelectItem>
181
+ ))}
182
+ </SelectContent>
183
+ </Select>
184
+ <DateRangePicker value={dateRange} onChange={setDateRange} />
185
+ <Input
186
+ placeholder={t("dispatch.transactionalEmail.sendLogToFilter")}
187
+ value={to}
188
+ onChange={(event) => setTo(event.target.value)}
189
+ className="w-44"
190
+ />
191
+ <Input
192
+ placeholder={t("dispatch.transactionalEmail.sendLogFromFilter")}
193
+ value={from}
194
+ onChange={(event) => setFrom(event.target.value)}
195
+ className="w-44"
196
+ />
197
+ <Select value={status} onValueChange={setStatus}>
198
+ <SelectTrigger className="w-32">
199
+ <SelectValue />
200
+ </SelectTrigger>
201
+ <SelectContent>
202
+ <SelectItem value="all">
203
+ {t("dispatch.transactionalEmail.sendLogAllStatuses")}
204
+ </SelectItem>
205
+ <SelectItem value="sent">
206
+ {t("dispatch.transactionalEmail.sendLogSent")}
207
+ </SelectItem>
208
+ <SelectItem value="failed">
209
+ {t("dispatch.transactionalEmail.sendLogFailed")}
210
+ </SelectItem>
211
+ </SelectContent>
212
+ </Select>
213
+ <Select value={provider} onValueChange={setProvider}>
214
+ <SelectTrigger className="w-36">
215
+ <SelectValue />
216
+ </SelectTrigger>
217
+ <SelectContent>
218
+ <SelectItem value="all">
219
+ {t("dispatch.transactionalEmail.sendLogAllProviders")}
220
+ </SelectItem>
221
+ <SelectItem value="resend">Resend</SelectItem>
222
+ <SelectItem value="sendgrid">SendGrid</SelectItem>
223
+ <SelectItem value="dev">Dev</SelectItem>
224
+ </SelectContent>
225
+ </Select>
226
+ {(to || from || status !== "all" || provider !== "all") && (
227
+ <Button
228
+ variant="ghost"
229
+ size="sm"
230
+ onClick={() => {
231
+ setTo("");
232
+ setFrom("");
233
+ setStatus("all");
234
+ setProvider("all");
235
+ }}
236
+ >
237
+ {t("dispatch.transactionalEmail.sendLogClearFilters")}
238
+ </Button>
239
+ )}
240
+ </div>
241
+
242
+ {query.isError ? (
243
+ <ActionQueryError
244
+ error={query.error}
245
+ onRetry={() => void query.refetch()}
246
+ />
247
+ ) : query.isLoading ? (
248
+ <Skeleton className="h-40 w-full" />
249
+ ) : entries.length === 0 ? (
250
+ <div className="rounded-xl border border-dashed px-4 py-8 text-center text-sm text-muted-foreground">
251
+ {t("dispatch.transactionalEmail.sendLogEmpty")}
252
+ </div>
253
+ ) : (
254
+ <>
255
+ <Table>
256
+ <TableHeader>
257
+ <TableRow>
258
+ <TableHead>
259
+ {t("dispatch.transactionalEmail.sendLogTimestamp")}
260
+ </TableHead>
261
+ <TableHead>
262
+ {t("dispatch.transactionalEmail.recipient")}
263
+ </TableHead>
264
+ <TableHead>{t("dispatch.transactionalEmail.sender")}</TableHead>
265
+ <TableHead>
266
+ {t("dispatch.transactionalEmail.sendLogTemplate")}
267
+ </TableHead>
268
+ <TableHead>{t("dispatch.transactionalEmail.status")}</TableHead>
269
+ <TableHead>
270
+ {t("dispatch.transactionalEmail.sendLogProvider")}
271
+ </TableHead>
272
+ </TableRow>
273
+ </TableHeader>
274
+ <TableBody>
275
+ {entries.map((entry) => (
276
+ <TableRow
277
+ key={entry.id}
278
+ className="cursor-pointer"
279
+ onClick={() => setSelected(entry)}
280
+ >
281
+ <TableCell className="text-xs text-muted-foreground">
282
+ {new Date(entry.createdAt).toLocaleString()}
283
+ </TableCell>
284
+ <TableCell className="text-xs">{entry.recipient}</TableCell>
285
+ <TableCell className="text-xs">{entry.sender}</TableCell>
286
+ <TableCell className="font-mono text-xs">
287
+ {entry.templateId ?? "—"}
288
+ </TableCell>
289
+ <TableCell>
290
+ <Badge
291
+ variant={
292
+ entry.status === "failed" ? "destructive" : "secondary"
293
+ }
294
+ >
295
+ {entry.status}
296
+ </Badge>
297
+ </TableCell>
298
+ <TableCell className="text-xs">{entry.provider}</TableCell>
299
+ </TableRow>
300
+ ))}
301
+ </TableBody>
302
+ </Table>
303
+ <div className="mt-3 flex justify-end gap-2">
304
+ <Button
305
+ variant="outline"
306
+ size="sm"
307
+ disabled={offset === 0}
308
+ onClick={() =>
309
+ setOffset((current) => Math.max(0, current - PAGE_SIZE))
310
+ }
311
+ >
312
+ {t("dispatch.transactionalEmail.sendLogPrevious")}
313
+ </Button>
314
+ <Button
315
+ variant="outline"
316
+ size="sm"
317
+ disabled={!hasMore}
318
+ onClick={() => setOffset((current) => current + PAGE_SIZE)}
319
+ >
320
+ {t("dispatch.transactionalEmail.sendLogNext")}
321
+ </Button>
322
+ </div>
323
+ </>
324
+ )}
325
+
326
+ <SendLogDetailDialog
327
+ entry={selected}
328
+ open={selected !== null}
329
+ onOpenChange={(next) => {
330
+ if (!next) setSelected(null);
331
+ }}
332
+ />
333
+ </div>
334
+ );
335
+ }
@@ -34,6 +34,7 @@ import {
34
34
  type ProviderMetricsResult,
35
35
  } from "../../components/transactional-email-metrics";
36
36
  import { EmailPreviewPane } from "../../components/transactional-email-preview";
37
+ import { SendLogSection } from "../../components/transactional-email-send-log";
37
38
  import { Alert, AlertDescription, AlertTitle } from "../../components/ui/alert";
38
39
  import { Button } from "../../components/ui/button";
39
40
  import {
@@ -513,6 +514,22 @@ export default function TransactionalEmailRoute() {
513
514
  </div>
514
515
  ) : null}
515
516
 
517
+ {apps.length > 0 ? (
518
+ <Collapsible defaultOpen className="rounded-2xl bg-card">
519
+ <CollapsibleTrigger className="group flex w-full cursor-pointer items-center justify-between gap-3 p-5 text-left hover:bg-muted/20">
520
+ <span className="flex items-center gap-2">
521
+ <IconChevronRight className="h-4 w-4 shrink-0 text-muted-foreground transition-transform group-data-[state=open]:rotate-90" />
522
+ <span className="text-sm font-medium text-foreground">
523
+ {t("dispatch.transactionalEmail.sendLogTitle")}
524
+ </span>
525
+ </span>
526
+ </CollapsibleTrigger>
527
+ <CollapsibleContent className="border-t px-5 pb-5 pt-4">
528
+ <SendLogSection apps={apps} />
529
+ </CollapsibleContent>
530
+ </Collapsible>
531
+ ) : null}
532
+
516
533
  <AppEmailCard
517
534
  name={t("dispatch.transactionalEmail.sharedTitle")}
518
535
  path={t("dispatch.transactionalEmail.sharedSubtitle")}