@agent-native/core 0.141.3 → 0.141.4
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.
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/.agents/skills/analysis-workspace/SKILL.md +1 -1
- package/corpus/templates/analytics/actions/gong-calls.ts +56 -18
- package/corpus/templates/analytics/changelog/2026-08-05-analytics-queries-keep-every-table-tenant-scoped.md +6 -0
- package/corpus/templates/analytics/changelog/2026-08-05-fixed-sign-in-and-dashboard-requests-failing-when-background.md +6 -0
- package/corpus/templates/analytics/changelog/2026-08-05-historical-rollups-stay-complete-while-live-events-arrive.md +6 -0
- package/corpus/templates/analytics/server/db/schema.ts +1 -0
- package/corpus/templates/analytics/server/lib/dashboard-report.ts +2 -0
- package/corpus/templates/analytics/server/lib/emails.ts +51 -0
- package/corpus/templates/analytics/server/lib/first-party-analytics-health.ts +8 -12
- package/corpus/templates/analytics/server/lib/first-party-analytics-rollups.ts +9 -0
- package/corpus/templates/analytics/server/lib/first-party-analytics.ts +262 -13
- package/corpus/templates/analytics/server/lib/gong.ts +132 -33
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +1 -1
- package/corpus/templates/analytics/server/plugins/auth.ts +21 -2
- package/corpus/templates/analytics/server/plugins/db.ts +11 -3
- package/corpus/templates/analytics/server/plugins/transactional-emails.ts +5 -0
- package/corpus/templates/calendar/app/components/calendar/InlineEventPickers.tsx +17 -7
- package/corpus/templates/calendar/server/lib/booking-emails.ts +154 -70
- package/corpus/templates/calendar/server/lib/emails.ts +167 -0
- package/corpus/templates/calendar/server/lib/event-guest-notifications.ts +64 -28
- package/corpus/templates/calendar/server/plugins/transactional-emails.ts +5 -0
- package/corpus/templates/clips/actions/invite-member.ts +39 -16
- package/corpus/templates/clips/app/components/player/share-dialog.tsx +73 -1
- package/corpus/templates/clips/app/i18n/en-US.ts +3 -0
- package/corpus/templates/clips/app/routes/r.$recordingId.tsx +10 -25
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +2 -0
- package/corpus/templates/clips/changelog/2026-08-05-activity-is-now-the-default-clips-recording-tab-and-ai-setup.md +6 -0
- package/corpus/templates/clips/changelog/2026-08-05-clips-can-copy-a-linked-email-preview-for-public-recordings.md +6 -0
- package/corpus/templates/clips/changelog/2026-08-05-preparing-recording-now-uses-a-clean-shadow-free-status-card.md +6 -0
- package/corpus/templates/clips/desktop/src/styles.css +0 -3
- package/corpus/templates/clips/server/lib/emails.ts +256 -0
- package/corpus/templates/clips/server/lib/transactional-email-templates.ts +24 -0
- package/corpus/templates/clips/server/plugins/transactional-email-catalog.ts +5 -0
- package/corpus/templates/clips/server/routes/api/thumbnail/[recordingId].get.ts +4 -1
- package/corpus/templates/clips/shared/email-preview.ts +61 -0
- package/corpus/templates/content/server/lib/comment-notifications.ts +57 -26
- package/corpus/templates/content/server/lib/emails.ts +70 -0
- package/corpus/templates/content/server/plugins/transactional-emails.ts +5 -0
- package/corpus/templates/design/actions/import-design-tokens.ts +11 -8
- package/corpus/templates/design/actions/index-design-tokens.ts +13 -6
- package/corpus/templates/design/changelog/2026-08-05-design-token-imports-classify-typography-dimensions-correctly.md +6 -0
- package/corpus/templates/dispatch/app/routes/transactional-email.$appId.$id.tsx +4 -0
- package/corpus/templates/dispatch/app/routes/transactional-email.tsx +4 -0
- package/corpus/templates/dispatch/changelog/2026-08-05-transactional-email-catalogs-and-delivery-activity-are-available-again.md +6 -0
- package/corpus/templates/factory/AGENTS.md +18 -17
- package/corpus/templates/factory/actions/get-factory-automation-health.ts +50 -0
- package/corpus/templates/factory/actions/get-triage-config.ts +8 -0
- package/corpus/templates/factory/actions/save-triage-config.ts +26 -0
- package/corpus/templates/factory/app/i18n/en-US.ts +27 -0
- package/corpus/templates/factory/app/routes/factory.tsx +376 -206
- package/corpus/templates/factory/changelog/2026-08-05-factory-now-keeps-production-automations-running-shows-sched.md +6 -0
- package/corpus/templates/factory/server/db/schema.ts +6 -0
- package/corpus/templates/factory/server/plugins/factory-migrations.ts +10 -0
- package/corpus/templates/factory/server/plugins/factory-scheduler-job.ts +141 -2
- package/corpus/templates/forms/app/pages/FormsListPage.tsx +41 -21
- package/corpus/templates/forms/changelog/2026-08-05-form-links-can-now-be-cmd-clicked-to-open-in-a-new-tab.md +6 -0
- package/corpus/templates/forms/server/lib/emails.ts +52 -0
- package/corpus/templates/forms/server/lib/response-email.ts +2 -0
- package/corpus/templates/forms/server/plugins/transactional-emails.ts +5 -0
- package/corpus/templates/plan/actions/request-plan-access.ts +33 -12
- package/corpus/templates/plan/server/lib/comment-notifications.ts +53 -27
- package/corpus/templates/plan/server/lib/emails.ts +67 -0
- package/corpus/templates/plan/server/plugins/transactional-emails.ts +5 -0
- package/corpus/templates/slides/actions/import-from-url.ts +1 -0
- package/corpus/templates/slides/app/components/editor/NewDeckReferenceStep.tsx +492 -381
- package/corpus/templates/slides/app/components/editor/PromptDialog.tsx +8 -7
- package/corpus/templates/slides/app/components/layout/Layout.tsx +27 -23
- package/corpus/templates/slides/app/components/onboarding/FirstDeckOnboardingFlow.tsx +313 -0
- package/corpus/templates/slides/app/components/ui/command.tsx +1 -0
- package/corpus/templates/slides/app/i18n/en-US.ts +8 -2
- package/corpus/templates/slides/app/lib/create-deck-generation.ts +305 -0
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +12 -12
- package/corpus/templates/slides/app/pages/Index.tsx +127 -137
- package/corpus/templates/slides/app/root.tsx +8 -0
- package/corpus/templates/slides/changelog/2026-08-05-deck-browsing-keeps-all-and-mine-in-the-header-with-a-tighte.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-05-new-deck-creation-now-flows-through-a-clean-full-screen-refe.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-05-website-imports-now-include-linked-stylesheets-for-more-accu.md +6 -0
- package/corpus/templates/slides/server/lib/comment-notifications.ts +44 -21
- package/corpus/templates/slides/server/lib/emails.ts +40 -0
- package/corpus/templates/slides/server/plugins/transactional-emails.ts +5 -0
- package/dist/brand-kit/index.d.ts +1 -0
- package/dist/brand-kit/index.js +1 -0
- package/dist/brand-kit/tokens.d.ts +91 -0
- package/dist/brand-kit/tokens.js +279 -0
- package/dist/brand-kit/types.d.ts +25 -0
- package/dist/client/AssistantChat.js +7 -7
- package/dist/client/onboarding/FirstRunOnboarding.js +26 -2
- package/dist/client/onboarding/first-run-registry.d.ts +25 -0
- package/dist/client/onboarding/first-run-registry.js +18 -0
- package/dist/client/onboarding/index.d.ts +2 -0
- package/dist/client/onboarding/index.js +1 -0
- package/dist/collab/routes.d.ts +1 -1
- package/dist/deploy/build.d.ts +8 -0
- package/dist/deploy/build.js +92 -3
- package/dist/deploy/workspace-deploy.js +86 -3
- package/dist/email-catalog/actions/list-email-activity.d.ts +5 -0
- package/dist/email-catalog/actions/list-email-activity.js +19 -0
- package/dist/email-catalog/actions/list-email-engagement.d.ts +5 -0
- package/dist/email-catalog/actions/list-email-engagement.js +15 -0
- package/dist/email-catalog/actions/list-email-log.d.ts +7 -0
- package/dist/email-catalog/actions/list-email-log.js +23 -0
- package/dist/email-catalog/actions/list-transactional-emails.d.ts +22 -0
- package/dist/email-catalog/actions/list-transactional-emails.js +67 -0
- package/dist/email-catalog/actions/render-transactional-email-preview.d.ts +9 -0
- package/dist/email-catalog/actions/render-transactional-email-preview.js +25 -0
- package/dist/email-catalog/authorize.d.ts +1 -0
- package/dist/email-catalog/authorize.js +9 -0
- package/dist/email-catalog/log.d.ts +60 -0
- package/dist/email-catalog/log.js +142 -0
- package/dist/email-catalog/provider-metrics.d.ts +34 -0
- package/dist/email-catalog/provider-metrics.js +182 -0
- package/dist/email-catalog/registry.d.ts +78 -0
- package/dist/email-catalog/registry.js +70 -0
- package/dist/email-catalog/schema.d.ts +231 -0
- package/dist/email-catalog/schema.js +53 -0
- package/dist/email-catalog/system-emails.d.ts +13 -0
- package/dist/email-catalog/system-emails.js +67 -0
- package/dist/jobs/index.d.ts +2 -0
- package/dist/jobs/index.js +2 -0
- package/dist/jobs/run-history.js +48 -1
- package/dist/jobs/scheduler-dispatch.d.ts +7 -0
- package/dist/jobs/scheduler-dispatch.js +7 -0
- package/dist/jobs/scheduler-health.d.ts +22 -0
- package/dist/jobs/scheduler-health.js +172 -0
- package/dist/jobs/scheduler.js +67 -2
- package/dist/localization/default-messages.d.ts +45 -0
- package/dist/localization/default-messages.js +45 -0
- package/dist/mcp/screen-memory-stdio.d.ts +7 -7
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +5 -5
- package/dist/org/handlers.js +9 -1
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +10 -10
- package/dist/provider-api/actions/provider-api.d.ts +8 -8
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/action-discovery.js +22 -0
- package/dist/server/agent-chat/recurring-jobs-runtime.d.ts +7 -1
- package/dist/server/agent-chat/recurring-jobs-runtime.js +12 -0
- package/dist/server/agent-chat-plugin.d.ts +2 -1
- package/dist/server/agent-chat-plugin.js +39 -1
- package/dist/server/better-auth-instance.js +17 -2
- package/dist/server/design-token-utils.d.ts +6 -1
- package/dist/server/design-token-utils.js +252 -59
- package/dist/server/email.d.ts +11 -0
- package/dist/server/email.js +54 -3
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +1 -0
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/vite/client.js +20 -14
- package/package.json +2 -1
|
@@ -12,6 +12,7 @@ import { isInBackgroundFunctionRuntime } from "@agent-native/core/server";
|
|
|
12
12
|
// startup so the dashboard / analysis share actions know where to dispatch.
|
|
13
13
|
import "../db/index.js";
|
|
14
14
|
import * as schema from "../db/schema.js";
|
|
15
|
+
import { FIRST_PARTY_ANALYTICS_ROLLUP_LOCK_KEY } from "../lib/first-party-analytics-rollups.js";
|
|
15
16
|
import { repairPersistedFirstPartyDashboardQueries } from "../lib/first-party-dashboard-repair.js";
|
|
16
17
|
|
|
17
18
|
/**
|
|
@@ -169,10 +170,11 @@ async function runHistoricalAnalyticsRollupBackfill() {
|
|
|
169
170
|
// connections while waiting for the winner. A lock loser defers without
|
|
170
171
|
// recording the migration so a failed winner can be retried safely.
|
|
171
172
|
// The monotonic rollup upsert below keeps a live increment from being
|
|
172
|
-
// overwritten if ingest commits during the historical snapshot.
|
|
173
|
+
// overwritten if ingest commits during the historical snapshot. Live
|
|
174
|
+
// ingest takes this same lock before inserting its raw event.
|
|
173
175
|
const lockResult = await tx.execute({
|
|
174
176
|
sql: "SELECT pg_try_advisory_xact_lock(hashtextextended(?, 0::bigint)) AS acquired",
|
|
175
|
-
args: [
|
|
177
|
+
args: [FIRST_PARTY_ANALYTICS_ROLLUP_LOCK_KEY],
|
|
176
178
|
});
|
|
177
179
|
const acquired = lockResult.rows[0]?.acquired;
|
|
178
180
|
if (acquired !== true && acquired !== "t") {
|
|
@@ -1570,10 +1572,16 @@ const runAnalyticsMigrations = runMigrations(
|
|
|
1570
1572
|
* swallowed so it can never fail boot.
|
|
1571
1573
|
*/
|
|
1572
1574
|
export default async (nitroApp: any): Promise<void> => {
|
|
1573
|
-
await runAnalyticsMigrations(nitroApp);
|
|
1574
1575
|
if (isInBackgroundFunctionRuntime()) {
|
|
1576
|
+
// Durable workers execute signed internal routes against a schema owned by
|
|
1577
|
+
// the regular server. A second migration runner only adds a Neon pool
|
|
1578
|
+
// probe to every worker cold start.
|
|
1579
|
+
console.info(
|
|
1580
|
+
"[db] Skipping Analytics migrations in durable background runtime",
|
|
1581
|
+
);
|
|
1575
1582
|
return;
|
|
1576
1583
|
}
|
|
1584
|
+
await runAnalyticsMigrations(nitroApp);
|
|
1577
1585
|
try {
|
|
1578
1586
|
if (await repairPersistedFirstPartyDashboardQueries()) {
|
|
1579
1587
|
console.info(
|
|
@@ -36,7 +36,7 @@ function formatTimeValue(value: string) {
|
|
|
36
36
|
const period = hourValue >= 12 ? "PM" : "AM";
|
|
37
37
|
const hour = hourValue % 12 || 12;
|
|
38
38
|
return minuteValue === 0
|
|
39
|
-
? `${hour} ${period}`
|
|
39
|
+
? `${hour}:00 ${period}`
|
|
40
40
|
: `${hour}:${String(minuteValue).padStart(2, "0")} ${period}`;
|
|
41
41
|
}
|
|
42
42
|
|
|
@@ -118,12 +118,17 @@ export function TimePickerPopover({
|
|
|
118
118
|
}}
|
|
119
119
|
>
|
|
120
120
|
<span>{formatTimeValue(option)}</span>
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
{getOptionMeta(option)}
|
|
121
|
+
<span className="ml-3 flex min-w-0 items-center gap-2 text-muted-foreground">
|
|
122
|
+
{getOptionMeta?.(option) && (
|
|
123
|
+
<span className="truncate">{getOptionMeta(option)}</span>
|
|
124
|
+
)}
|
|
125
|
+
<span
|
|
126
|
+
aria-hidden="true"
|
|
127
|
+
className="flex size-3.5 shrink-0 items-center justify-center"
|
|
128
|
+
>
|
|
129
|
+
{selected && <IconCheck className="size-3.5" />}
|
|
124
130
|
</span>
|
|
125
|
-
|
|
126
|
-
{selected && <IconCheck className="ml-2 size-3.5 shrink-0" />}
|
|
131
|
+
</span>
|
|
127
132
|
</button>
|
|
128
133
|
);
|
|
129
134
|
})}
|
|
@@ -338,7 +343,12 @@ export function RepeatPicker({
|
|
|
338
343
|
<span>{option.label}</span>
|
|
339
344
|
<span className="ml-3 flex items-center gap-2 text-muted-foreground">
|
|
340
345
|
{option.meta}
|
|
341
|
-
|
|
346
|
+
<span
|
|
347
|
+
aria-hidden="true"
|
|
348
|
+
className="flex size-3.5 shrink-0 items-center justify-center"
|
|
349
|
+
>
|
|
350
|
+
{selected && <IconCheck className="size-3.5" />}
|
|
351
|
+
</span>
|
|
342
352
|
</span>
|
|
343
353
|
</button>
|
|
344
354
|
);
|
|
@@ -11,6 +11,12 @@ import {
|
|
|
11
11
|
DEFAULT_BOOKING_TIMEZONE,
|
|
12
12
|
safeBookingTimeZone,
|
|
13
13
|
} from "./booking-timezone.js";
|
|
14
|
+
import {
|
|
15
|
+
CALENDAR_BOOKING_CANCELLED_EMAIL_ID,
|
|
16
|
+
CALENDAR_BOOKING_CANCELLED_HOST_EMAIL_ID,
|
|
17
|
+
CALENDAR_BOOKING_CONFIRMED_EMAIL_ID,
|
|
18
|
+
CALENDAR_BOOKING_RECEIVED_EMAIL_ID,
|
|
19
|
+
} from "./emails.js";
|
|
14
20
|
|
|
15
21
|
function stripCrlf(value: string | undefined): string {
|
|
16
22
|
return (value ?? "").replace(/[\r\n]+/g, " ").trim();
|
|
@@ -57,6 +63,128 @@ async function sendBestEffort(
|
|
|
57
63
|
}
|
|
58
64
|
}
|
|
59
65
|
|
|
66
|
+
export function renderBookingConfirmedEmail({
|
|
67
|
+
title,
|
|
68
|
+
when,
|
|
69
|
+
host,
|
|
70
|
+
manageUrl,
|
|
71
|
+
meetingLink,
|
|
72
|
+
}: {
|
|
73
|
+
title: string;
|
|
74
|
+
when: string;
|
|
75
|
+
host: string;
|
|
76
|
+
manageUrl: string;
|
|
77
|
+
meetingLink?: string | null;
|
|
78
|
+
}) {
|
|
79
|
+
const paragraphs = [
|
|
80
|
+
`You're booked for ${emailStrong(title)} with ${emailStrong(host)}.`,
|
|
81
|
+
`Time: ${emailStrong(when)}.`,
|
|
82
|
+
];
|
|
83
|
+
if (meetingLink) {
|
|
84
|
+
paragraphs.push(`Meeting link: ${emailLink("Join meeting", meetingLink)}.`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
subject: `Confirmed: ${title}`,
|
|
89
|
+
...renderEmail({
|
|
90
|
+
preheader: `You're booked for ${title} on ${when}.`,
|
|
91
|
+
heading: "Your meeting is booked",
|
|
92
|
+
paragraphs,
|
|
93
|
+
cta: { label: "Manage booking", url: manageUrl },
|
|
94
|
+
footer:
|
|
95
|
+
"Use the manage link if you need to cancel or reschedule this meeting.",
|
|
96
|
+
}),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function renderBookingReceivedEmail({
|
|
101
|
+
title,
|
|
102
|
+
when,
|
|
103
|
+
attendeeName,
|
|
104
|
+
attendee,
|
|
105
|
+
manageUrl,
|
|
106
|
+
meetingLink,
|
|
107
|
+
}: {
|
|
108
|
+
title: string;
|
|
109
|
+
when: string;
|
|
110
|
+
attendeeName: string;
|
|
111
|
+
attendee: string;
|
|
112
|
+
manageUrl: string;
|
|
113
|
+
meetingLink?: string | null;
|
|
114
|
+
}) {
|
|
115
|
+
return {
|
|
116
|
+
subject: `New booking: ${title}`,
|
|
117
|
+
...renderEmail({
|
|
118
|
+
preheader: `${attendeeName} booked ${title} on ${when}.`,
|
|
119
|
+
heading: "New booking",
|
|
120
|
+
paragraphs: [
|
|
121
|
+
`${emailStrong(attendeeName)} booked ${emailStrong(title)}.`,
|
|
122
|
+
`Time: ${emailStrong(when)}.`,
|
|
123
|
+
`Guest: ${emailStrong(attendee)}.`,
|
|
124
|
+
...(meetingLink
|
|
125
|
+
? [`Meeting link: ${emailLink("Join meeting", meetingLink)}.`]
|
|
126
|
+
: []),
|
|
127
|
+
],
|
|
128
|
+
cta: { label: "View booking", url: manageUrl },
|
|
129
|
+
footer: "This booking was created from your calendar booking link.",
|
|
130
|
+
}),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function renderBookingCancelledEmail({
|
|
135
|
+
title,
|
|
136
|
+
when,
|
|
137
|
+
host,
|
|
138
|
+
bookAgainUrl,
|
|
139
|
+
}: {
|
|
140
|
+
title: string;
|
|
141
|
+
when: string;
|
|
142
|
+
host: string;
|
|
143
|
+
bookAgainUrl?: string;
|
|
144
|
+
}) {
|
|
145
|
+
return {
|
|
146
|
+
subject: `Cancelled: ${title}`,
|
|
147
|
+
...renderEmail({
|
|
148
|
+
preheader: `${title} on ${when} was cancelled.`,
|
|
149
|
+
heading: "Your meeting was cancelled",
|
|
150
|
+
paragraphs: [
|
|
151
|
+
`${emailStrong(title)} with ${emailStrong(host || "the host")} has been cancelled.`,
|
|
152
|
+
`Original time: ${emailStrong(when)}.`,
|
|
153
|
+
],
|
|
154
|
+
cta: bookAgainUrl
|
|
155
|
+
? { label: "Book another time", url: bookAgainUrl }
|
|
156
|
+
: undefined,
|
|
157
|
+
footer: "If this was unexpected, contact the meeting host.",
|
|
158
|
+
}),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function renderBookingCancelledHostEmail({
|
|
163
|
+
title,
|
|
164
|
+
when,
|
|
165
|
+
attendeeName,
|
|
166
|
+
attendee,
|
|
167
|
+
}: {
|
|
168
|
+
title: string;
|
|
169
|
+
when: string;
|
|
170
|
+
attendeeName: string;
|
|
171
|
+
attendee: string;
|
|
172
|
+
}) {
|
|
173
|
+
return {
|
|
174
|
+
subject: `Cancelled booking: ${title}`,
|
|
175
|
+
...renderEmail({
|
|
176
|
+
preheader: `${attendeeName}'s booking for ${title} was cancelled.`,
|
|
177
|
+
heading: "Booking cancelled",
|
|
178
|
+
paragraphs: [
|
|
179
|
+
`${emailStrong(attendeeName)}'s booking for ${emailStrong(title)} was cancelled.`,
|
|
180
|
+
`Original time: ${emailStrong(when)}.`,
|
|
181
|
+
`Guest: ${emailStrong(attendee)}.`,
|
|
182
|
+
],
|
|
183
|
+
footer: "No further action is needed.",
|
|
184
|
+
}),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
60
188
|
export async function sendBookingConfirmationEmails({
|
|
61
189
|
booking,
|
|
62
190
|
hostEmail,
|
|
@@ -74,54 +202,31 @@ export async function sendBookingConfirmationEmails({
|
|
|
74
202
|
const attendee = stripCrlf(booking.email);
|
|
75
203
|
const attendeeName = stripCrlf(booking.name) || "there";
|
|
76
204
|
|
|
77
|
-
const attendeeParagraphs = [
|
|
78
|
-
`You're booked for ${emailStrong(title)} with ${emailStrong(host)}.`,
|
|
79
|
-
`Time: ${emailStrong(when)}.`,
|
|
80
|
-
];
|
|
81
|
-
if (booking.meetingLink) {
|
|
82
|
-
attendeeParagraphs.push(
|
|
83
|
-
`Meeting link: ${emailLink("Join meeting", booking.meetingLink)}.`,
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const attendeeEmail = renderEmail({
|
|
88
|
-
preheader: `You're booked for ${title} on ${when}.`,
|
|
89
|
-
heading: "Your meeting is booked",
|
|
90
|
-
paragraphs: attendeeParagraphs,
|
|
91
|
-
cta: { label: "Manage booking", url: manageUrl },
|
|
92
|
-
footer:
|
|
93
|
-
"Use the manage link if you need to cancel or reschedule this meeting.",
|
|
94
|
-
});
|
|
95
|
-
|
|
96
205
|
await sendBestEffort("attendee confirmation", {
|
|
97
206
|
to: attendee,
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
207
|
+
...renderBookingConfirmedEmail({
|
|
208
|
+
title,
|
|
209
|
+
when,
|
|
210
|
+
host,
|
|
211
|
+
manageUrl,
|
|
212
|
+
meetingLink: booking.meetingLink,
|
|
213
|
+
}),
|
|
101
214
|
replyTo: host,
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const hostEmailMessage = renderEmail({
|
|
105
|
-
preheader: `${attendeeName} booked ${title} on ${when}.`,
|
|
106
|
-
heading: "New booking",
|
|
107
|
-
paragraphs: [
|
|
108
|
-
`${emailStrong(attendeeName)} booked ${emailStrong(title)}.`,
|
|
109
|
-
`Time: ${emailStrong(when)}.`,
|
|
110
|
-
`Guest: ${emailStrong(attendee)}.`,
|
|
111
|
-
...(booking.meetingLink
|
|
112
|
-
? [`Meeting link: ${emailLink("Join meeting", booking.meetingLink)}.`]
|
|
113
|
-
: []),
|
|
114
|
-
],
|
|
115
|
-
cta: { label: "View booking", url: manageUrl },
|
|
116
|
-
footer: "This booking was created from your calendar booking link.",
|
|
215
|
+
templateId: CALENDAR_BOOKING_CONFIRMED_EMAIL_ID,
|
|
117
216
|
});
|
|
118
217
|
|
|
119
218
|
await sendBestEffort("host notification", {
|
|
120
219
|
to: host,
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
220
|
+
...renderBookingReceivedEmail({
|
|
221
|
+
title,
|
|
222
|
+
when,
|
|
223
|
+
attendeeName,
|
|
224
|
+
attendee,
|
|
225
|
+
manageUrl,
|
|
226
|
+
meetingLink: booking.meetingLink,
|
|
227
|
+
}),
|
|
124
228
|
replyTo: attendee,
|
|
229
|
+
templateId: CALENDAR_BOOKING_RECEIVED_EMAIL_ID,
|
|
125
230
|
});
|
|
126
231
|
}
|
|
127
232
|
|
|
@@ -142,45 +247,24 @@ export async function sendBookingCancellationEmails({
|
|
|
142
247
|
const attendee = stripCrlf(booking.email);
|
|
143
248
|
const attendeeName = stripCrlf(booking.name) || "The guest";
|
|
144
249
|
|
|
145
|
-
const attendeeEmail = renderEmail({
|
|
146
|
-
preheader: `${title} on ${when} was cancelled.`,
|
|
147
|
-
heading: "Your meeting was cancelled",
|
|
148
|
-
paragraphs: [
|
|
149
|
-
`${emailStrong(title)} with ${emailStrong(host || "the host")} has been cancelled.`,
|
|
150
|
-
`Original time: ${emailStrong(when)}.`,
|
|
151
|
-
],
|
|
152
|
-
cta: bookAgainUrl
|
|
153
|
-
? { label: "Book another time", url: bookAgainUrl }
|
|
154
|
-
: undefined,
|
|
155
|
-
footer: "If this was unexpected, contact the meeting host.",
|
|
156
|
-
});
|
|
157
|
-
|
|
158
250
|
await sendBestEffort("attendee cancellation", {
|
|
159
251
|
to: attendee,
|
|
160
|
-
|
|
161
|
-
html: attendeeEmail.html,
|
|
162
|
-
text: attendeeEmail.text,
|
|
252
|
+
...renderBookingCancelledEmail({ title, when, host, bookAgainUrl }),
|
|
163
253
|
replyTo: host || undefined,
|
|
254
|
+
templateId: CALENDAR_BOOKING_CANCELLED_EMAIL_ID,
|
|
164
255
|
});
|
|
165
256
|
|
|
166
257
|
if (!host) return;
|
|
167
258
|
|
|
168
|
-
const hostEmailMessage = renderEmail({
|
|
169
|
-
preheader: `${attendeeName}'s booking for ${title} was cancelled.`,
|
|
170
|
-
heading: "Booking cancelled",
|
|
171
|
-
paragraphs: [
|
|
172
|
-
`${emailStrong(attendeeName)}'s booking for ${emailStrong(title)} was cancelled.`,
|
|
173
|
-
`Original time: ${emailStrong(when)}.`,
|
|
174
|
-
`Guest: ${emailStrong(attendee)}.`,
|
|
175
|
-
],
|
|
176
|
-
footer: "No further action is needed.",
|
|
177
|
-
});
|
|
178
|
-
|
|
179
259
|
await sendBestEffort("host cancellation notification", {
|
|
180
260
|
to: host,
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
261
|
+
...renderBookingCancelledHostEmail({
|
|
262
|
+
title,
|
|
263
|
+
when,
|
|
264
|
+
attendeeName,
|
|
265
|
+
attendee,
|
|
266
|
+
}),
|
|
184
267
|
replyTo: attendee,
|
|
268
|
+
templateId: CALENDAR_BOOKING_CANCELLED_HOST_EMAIL_ID,
|
|
185
269
|
});
|
|
186
270
|
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catalog entries for the transactional emails Calendar sends.
|
|
3
|
+
*
|
|
4
|
+
* Registered from `server/plugins/transactional-emails.ts` so Dispatch can list
|
|
5
|
+
* and preview them without the app having sent anything yet.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { defineTransactionalEmail } from "@agent-native/core/email-catalog";
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
renderBookingCancelledEmail,
|
|
12
|
+
renderBookingCancelledHostEmail,
|
|
13
|
+
renderBookingConfirmedEmail,
|
|
14
|
+
renderBookingReceivedEmail,
|
|
15
|
+
} from "./booking-emails.js";
|
|
16
|
+
import { renderEventGuestNote } from "./event-guest-notifications.js";
|
|
17
|
+
|
|
18
|
+
/** Obviously-fake sample data — these render in a preview pane, never send. */
|
|
19
|
+
const SAMPLE_TITLE = "Intro call";
|
|
20
|
+
const SAMPLE_WHEN = "Tuesday, March 4, 2025, 10:00 AM - 10:30 AM PST";
|
|
21
|
+
const SAMPLE_HOST = "dana.hill@example.com";
|
|
22
|
+
const SAMPLE_GUEST = "sam.rivera@example.com";
|
|
23
|
+
const SAMPLE_GUEST_NAME = "Sam Rivera";
|
|
24
|
+
const SAMPLE_MANAGE_URL = "https://example.com/book/dana/manage/sample-token";
|
|
25
|
+
const SAMPLE_BOOK_AGAIN_URL = "https://example.com/book/dana/intro-call";
|
|
26
|
+
const SAMPLE_MEETING_LINK = "https://meet.example.com/sample-intro-call";
|
|
27
|
+
|
|
28
|
+
export const CALENDAR_BOOKING_CONFIRMED_EMAIL_ID = "calendar.booking-confirmed";
|
|
29
|
+
export const CALENDAR_BOOKING_RECEIVED_EMAIL_ID = "calendar.booking-received";
|
|
30
|
+
export const CALENDAR_BOOKING_CANCELLED_EMAIL_ID = "calendar.booking-cancelled";
|
|
31
|
+
export const CALENDAR_BOOKING_CANCELLED_HOST_EMAIL_ID =
|
|
32
|
+
"calendar.booking-cancelled-host";
|
|
33
|
+
export const CALENDAR_EVENT_UPDATE_NOTE_EMAIL_ID = "calendar.event-update-note";
|
|
34
|
+
export const CALENDAR_EVENT_CANCELLATION_NOTE_EMAIL_ID =
|
|
35
|
+
"calendar.event-cancellation-note";
|
|
36
|
+
|
|
37
|
+
let registered = false;
|
|
38
|
+
|
|
39
|
+
export function registerCalendarEmails(): void {
|
|
40
|
+
if (registered) return;
|
|
41
|
+
registered = true;
|
|
42
|
+
|
|
43
|
+
defineTransactionalEmail({
|
|
44
|
+
id: CALENDAR_BOOKING_CONFIRMED_EMAIL_ID,
|
|
45
|
+
name: "Booking confirmed (guest)",
|
|
46
|
+
trigger:
|
|
47
|
+
"A guest completes a booking on a public `/book/{username}/{slug}` page and the booking row is created.",
|
|
48
|
+
recipientLabel: "Booking guest",
|
|
49
|
+
recipient:
|
|
50
|
+
"The email address the guest typed into the booking form (`booking.email`).",
|
|
51
|
+
senderLabel: "Default, reply-to host",
|
|
52
|
+
sender:
|
|
53
|
+
"The configured EMAIL_FROM, with reply-to set to the booking link owner's address.",
|
|
54
|
+
preview: () =>
|
|
55
|
+
renderBookingConfirmedEmail({
|
|
56
|
+
title: SAMPLE_TITLE,
|
|
57
|
+
when: SAMPLE_WHEN,
|
|
58
|
+
host: SAMPLE_HOST,
|
|
59
|
+
manageUrl: SAMPLE_MANAGE_URL,
|
|
60
|
+
meetingLink: SAMPLE_MEETING_LINK,
|
|
61
|
+
}),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
defineTransactionalEmail({
|
|
65
|
+
id: CALENDAR_BOOKING_RECEIVED_EMAIL_ID,
|
|
66
|
+
name: "New booking (host)",
|
|
67
|
+
trigger:
|
|
68
|
+
"Sent alongside the guest confirmation, immediately after a public booking is created.",
|
|
69
|
+
recipientLabel: "Booking link owner",
|
|
70
|
+
recipient:
|
|
71
|
+
"The owner of the booking link, looked up from the link's slug at send time.",
|
|
72
|
+
senderLabel: "Default, reply-to guest",
|
|
73
|
+
sender:
|
|
74
|
+
"The configured EMAIL_FROM, with reply-to set to the guest so the host can reply directly.",
|
|
75
|
+
preview: () =>
|
|
76
|
+
renderBookingReceivedEmail({
|
|
77
|
+
title: SAMPLE_TITLE,
|
|
78
|
+
when: SAMPLE_WHEN,
|
|
79
|
+
attendeeName: SAMPLE_GUEST_NAME,
|
|
80
|
+
attendee: SAMPLE_GUEST,
|
|
81
|
+
manageUrl: SAMPLE_MANAGE_URL,
|
|
82
|
+
meetingLink: SAMPLE_MEETING_LINK,
|
|
83
|
+
}),
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
defineTransactionalEmail({
|
|
87
|
+
id: CALENDAR_BOOKING_CANCELLED_EMAIL_ID,
|
|
88
|
+
name: "Booking cancelled (guest)",
|
|
89
|
+
trigger:
|
|
90
|
+
"A booking that was not already cancelled is cancelled, either by the guest through the manage/cancel token link or by someone with access to the booking link.",
|
|
91
|
+
recipientLabel: "Booking guest",
|
|
92
|
+
recipient: "The guest address stored on the booking row.",
|
|
93
|
+
senderLabel: "Default, reply-to host",
|
|
94
|
+
sender:
|
|
95
|
+
"The configured EMAIL_FROM, with reply-to set to the host when a host address could be resolved from the link slug.",
|
|
96
|
+
preview: () =>
|
|
97
|
+
renderBookingCancelledEmail({
|
|
98
|
+
title: SAMPLE_TITLE,
|
|
99
|
+
when: SAMPLE_WHEN,
|
|
100
|
+
host: SAMPLE_HOST,
|
|
101
|
+
bookAgainUrl: SAMPLE_BOOK_AGAIN_URL,
|
|
102
|
+
}),
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
defineTransactionalEmail({
|
|
106
|
+
id: CALENDAR_BOOKING_CANCELLED_HOST_EMAIL_ID,
|
|
107
|
+
name: "Booking cancelled (host)",
|
|
108
|
+
trigger:
|
|
109
|
+
"Sent after the guest cancellation notice, and only when a host address could be resolved from the booking link slug.",
|
|
110
|
+
recipientLabel: "Booking link owner",
|
|
111
|
+
recipient: "The booking link owner's address.",
|
|
112
|
+
senderLabel: "Default, reply-to guest",
|
|
113
|
+
sender:
|
|
114
|
+
"The configured EMAIL_FROM, with reply-to set to the guest who was booked.",
|
|
115
|
+
preview: () =>
|
|
116
|
+
renderBookingCancelledHostEmail({
|
|
117
|
+
title: SAMPLE_TITLE,
|
|
118
|
+
when: SAMPLE_WHEN,
|
|
119
|
+
attendeeName: SAMPLE_GUEST_NAME,
|
|
120
|
+
attendee: SAMPLE_GUEST,
|
|
121
|
+
}),
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
defineTransactionalEmail({
|
|
125
|
+
id: CALENDAR_EVENT_UPDATE_NOTE_EMAIL_ID,
|
|
126
|
+
name: "Event update note",
|
|
127
|
+
trigger:
|
|
128
|
+
"`update-event` runs with a non-empty guest notification message. Google Calendar still sends its own update invite; this carries only the organizer's note.",
|
|
129
|
+
recipientLabel: "Event attendees",
|
|
130
|
+
recipient:
|
|
131
|
+
"Every attendee on the event with a syntactically valid address, excluding the organizer's own `self` attendee row. One email per address.",
|
|
132
|
+
senderLabel: "Default, reply-to organizer",
|
|
133
|
+
sender:
|
|
134
|
+
"The configured EMAIL_FROM, with reply-to set to the organizer running the update.",
|
|
135
|
+
preview: () =>
|
|
136
|
+
renderEventGuestNote({
|
|
137
|
+
title: "Design review",
|
|
138
|
+
organizer: "Dana Hill",
|
|
139
|
+
message: "Moving this an hour later so the whole team can join.",
|
|
140
|
+
when: SAMPLE_WHEN,
|
|
141
|
+
kind: "update",
|
|
142
|
+
htmlLink: "https://calendar.example.com/event/sample",
|
|
143
|
+
}),
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
defineTransactionalEmail({
|
|
147
|
+
id: CALENDAR_EVENT_CANCELLATION_NOTE_EMAIL_ID,
|
|
148
|
+
name: "Event cancellation note",
|
|
149
|
+
trigger:
|
|
150
|
+
"`delete-event` runs with guest notification requested and a non-empty message. Google Calendar sends the cancellation itself; this carries only the organizer's note.",
|
|
151
|
+
recipientLabel: "Event attendees",
|
|
152
|
+
recipient:
|
|
153
|
+
"Every attendee on the deleted event with a syntactically valid address, excluding the organizer's own `self` attendee row. One email per address.",
|
|
154
|
+
senderLabel: "Default, reply-to organizer",
|
|
155
|
+
sender:
|
|
156
|
+
"The configured EMAIL_FROM, with reply-to set to the organizer running the deletion.",
|
|
157
|
+
preview: () =>
|
|
158
|
+
renderEventGuestNote({
|
|
159
|
+
title: "Design review",
|
|
160
|
+
organizer: "Dana Hill",
|
|
161
|
+
message: "Cancelling this week — we will pick it up after the launch.",
|
|
162
|
+
when: SAMPLE_WHEN,
|
|
163
|
+
kind: "cancellation",
|
|
164
|
+
appliesTo: "all events in the series",
|
|
165
|
+
}),
|
|
166
|
+
});
|
|
167
|
+
}
|
|
@@ -6,6 +6,10 @@ import {
|
|
|
6
6
|
} from "@agent-native/core/server";
|
|
7
7
|
|
|
8
8
|
import type { CalendarEvent, DeleteEventScope } from "../../shared/api.js";
|
|
9
|
+
import {
|
|
10
|
+
CALENDAR_EVENT_CANCELLATION_NOTE_EMAIL_ID,
|
|
11
|
+
CALENDAR_EVENT_UPDATE_NOTE_EMAIL_ID,
|
|
12
|
+
} from "./emails.js";
|
|
9
13
|
|
|
10
14
|
export interface GuestNotificationResult {
|
|
11
15
|
requested: boolean;
|
|
@@ -96,6 +100,53 @@ function scopeLabel(scope: DeleteEventScope | undefined): string | undefined {
|
|
|
96
100
|
return "this and following events";
|
|
97
101
|
}
|
|
98
102
|
|
|
103
|
+
export function renderEventGuestNote({
|
|
104
|
+
title,
|
|
105
|
+
organizer,
|
|
106
|
+
message,
|
|
107
|
+
when,
|
|
108
|
+
kind,
|
|
109
|
+
appliesTo,
|
|
110
|
+
htmlLink,
|
|
111
|
+
}: {
|
|
112
|
+
title: string;
|
|
113
|
+
organizer: string;
|
|
114
|
+
message: string;
|
|
115
|
+
when: string;
|
|
116
|
+
kind: GuestNotificationKind;
|
|
117
|
+
appliesTo?: string;
|
|
118
|
+
htmlLink?: string | null;
|
|
119
|
+
}) {
|
|
120
|
+
const heading =
|
|
121
|
+
kind === "cancellation" ? "Event cancellation note" : "Event update note";
|
|
122
|
+
const subjectPrefix =
|
|
123
|
+
kind === "cancellation" ? "Cancellation note" : "Update note";
|
|
124
|
+
const eventAction = kind === "cancellation" ? "cancelling" : "updating";
|
|
125
|
+
const paragraphs = [
|
|
126
|
+
`${emailStrong(organizer)} added this note while ${eventAction} ${emailStrong(title)}.`,
|
|
127
|
+
messageParagraph(message),
|
|
128
|
+
`When: ${emailStrong(when)}.`,
|
|
129
|
+
];
|
|
130
|
+
if (appliesTo) {
|
|
131
|
+
paragraphs.push(`Applies to: ${emailStrong(appliesTo)}.`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
subject: `${subjectPrefix}: ${title}`,
|
|
136
|
+
...renderEmail({
|
|
137
|
+
preheader: `${subjectPrefix}: ${title}`,
|
|
138
|
+
heading,
|
|
139
|
+
paragraphs,
|
|
140
|
+
cta:
|
|
141
|
+
kind === "update" && htmlLink
|
|
142
|
+
? { label: "Open in Google Calendar", url: htmlLink }
|
|
143
|
+
: undefined,
|
|
144
|
+
footer:
|
|
145
|
+
"Google Calendar sends the calendar update separately. This message carries the organizer note.",
|
|
146
|
+
}),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
99
150
|
export async function sendEventGuestNotificationNote({
|
|
100
151
|
event,
|
|
101
152
|
organizerEmail,
|
|
@@ -138,33 +189,14 @@ export async function sendEventGuestNotificationNote({
|
|
|
138
189
|
};
|
|
139
190
|
}
|
|
140
191
|
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
kind
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
`${emailStrong(organizer)} added this note while ${eventAction} ${emailStrong(title)}.`,
|
|
150
|
-
messageParagraph(normalizedMessage),
|
|
151
|
-
`When: ${emailStrong(formatWhen(event))}.`,
|
|
152
|
-
];
|
|
153
|
-
const appliesTo = scopeLabel(scope);
|
|
154
|
-
if (appliesTo) {
|
|
155
|
-
paragraphs.push(`Applies to: ${emailStrong(appliesTo)}.`);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const rendered = renderEmail({
|
|
159
|
-
preheader: `${subjectPrefix}: ${title}`,
|
|
160
|
-
heading,
|
|
161
|
-
paragraphs,
|
|
162
|
-
cta:
|
|
163
|
-
kind === "update" && event.htmlLink
|
|
164
|
-
? { label: "Open in Google Calendar", url: event.htmlLink }
|
|
165
|
-
: undefined,
|
|
166
|
-
footer:
|
|
167
|
-
"Google Calendar sends the calendar update separately. This message carries the organizer note.",
|
|
192
|
+
const rendered = renderEventGuestNote({
|
|
193
|
+
title: stripCrlf(event.title) || "Calendar event",
|
|
194
|
+
organizer: stripCrlf(event.organizer?.displayName) || organizerEmail,
|
|
195
|
+
message: normalizedMessage,
|
|
196
|
+
when: formatWhen(event),
|
|
197
|
+
kind,
|
|
198
|
+
appliesTo: scopeLabel(scope),
|
|
199
|
+
htmlLink: event.htmlLink,
|
|
168
200
|
});
|
|
169
201
|
|
|
170
202
|
let sentCount = 0;
|
|
@@ -174,10 +206,14 @@ export async function sendEventGuestNotificationNote({
|
|
|
174
206
|
try {
|
|
175
207
|
await sendEmail({
|
|
176
208
|
to,
|
|
177
|
-
subject:
|
|
209
|
+
subject: rendered.subject,
|
|
178
210
|
html: rendered.html,
|
|
179
211
|
text: rendered.text,
|
|
180
212
|
replyTo: organizerEmail,
|
|
213
|
+
templateId:
|
|
214
|
+
kind === "cancellation"
|
|
215
|
+
? CALENDAR_EVENT_CANCELLATION_NOTE_EMAIL_ID
|
|
216
|
+
: CALENDAR_EVENT_UPDATE_NOTE_EMAIL_ID,
|
|
181
217
|
});
|
|
182
218
|
sentCount += 1;
|
|
183
219
|
} catch (error) {
|