@agent-native/core 0.141.2 → 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-durable-a2a-workers-avoid-startup-migration-contention.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 +35 -10
- 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/awareness.d.ts +2 -2
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/db/index.d.ts +1 -1
- package/dist/db/index.js +1 -1
- package/dist/db/migrations.d.ts +13 -3
- package/dist/db/migrations.js +19 -4
- 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 +3 -3
- 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/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 +69 -22
- package/dist/server/agent-engine-api-key-route.d.ts +1 -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 +3 -2
- package/dist/server/index.js +3 -2
- package/dist/vite/client.js +20 -14
- package/package.json +2 -1
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import {
|
|
2
|
+
deferMigration,
|
|
2
3
|
ensureAdditiveColumns,
|
|
3
4
|
getDialect,
|
|
4
5
|
getDbExec,
|
|
5
6
|
isPostgres,
|
|
6
7
|
runMigrations,
|
|
7
8
|
} from "@agent-native/core/db";
|
|
9
|
+
import { isInBackgroundFunctionRuntime } from "@agent-native/core/server";
|
|
8
10
|
|
|
9
11
|
// Side-effect import: ensures registerShareableResource runs on server
|
|
10
12
|
// startup so the dashboard / analysis share actions know where to dispatch.
|
|
11
13
|
import "../db/index.js";
|
|
12
14
|
import * as schema from "../db/schema.js";
|
|
15
|
+
import { FIRST_PARTY_ANALYTICS_ROLLUP_LOCK_KEY } from "../lib/first-party-analytics-rollups.js";
|
|
13
16
|
import { repairPersistedFirstPartyDashboardQueries } from "../lib/first-party-dashboard-repair.js";
|
|
14
17
|
|
|
15
18
|
/**
|
|
@@ -141,7 +144,17 @@ const ANALYTICS_ROLLUP_BACKFILL_STATEMENTS = {
|
|
|
141
144
|
],
|
|
142
145
|
} as const;
|
|
143
146
|
|
|
144
|
-
async function runHistoricalAnalyticsRollupBackfill()
|
|
147
|
+
async function runHistoricalAnalyticsRollupBackfill() {
|
|
148
|
+
// Durable agent workers share this plugin bundle with the regular server,
|
|
149
|
+
// but they must not become a second migration runner for an expensive
|
|
150
|
+
// historical scan. The keep-warm/regular server path owns this work.
|
|
151
|
+
if (isInBackgroundFunctionRuntime()) {
|
|
152
|
+
console.info(
|
|
153
|
+
"[db] Deferring historical Analytics rollup backfill from durable background runtime",
|
|
154
|
+
);
|
|
155
|
+
return deferMigration();
|
|
156
|
+
}
|
|
157
|
+
|
|
145
158
|
const db = getDbExec();
|
|
146
159
|
|
|
147
160
|
if (isPostgres()) {
|
|
@@ -151,27 +164,30 @@ async function runHistoricalAnalyticsRollupBackfill(): Promise<void> {
|
|
|
151
164
|
);
|
|
152
165
|
}
|
|
153
166
|
|
|
154
|
-
await db.transaction(async (tx) => {
|
|
167
|
+
const result = await db.transaction(async (tx) => {
|
|
155
168
|
// Only one serverless instance should perform the expensive scan. A
|
|
156
|
-
// try-lock
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
//
|
|
169
|
+
// A try-lock keeps competing cold starts from holding pooled Neon
|
|
170
|
+
// connections while waiting for the winner. A lock loser defers without
|
|
171
|
+
// recording the migration so a failed winner can be retried safely.
|
|
172
|
+
// The monotonic rollup upsert below keeps a live increment from being
|
|
173
|
+
// overwritten if ingest commits during the historical snapshot. Live
|
|
174
|
+
// ingest takes this same lock before inserting its raw event.
|
|
160
175
|
const lockResult = await tx.execute({
|
|
161
176
|
sql: "SELECT pg_try_advisory_xact_lock(hashtextextended(?, 0::bigint)) AS acquired",
|
|
162
|
-
args: [
|
|
177
|
+
args: [FIRST_PARTY_ANALYTICS_ROLLUP_LOCK_KEY],
|
|
163
178
|
});
|
|
164
179
|
const acquired = lockResult.rows[0]?.acquired;
|
|
165
180
|
if (acquired !== true && acquired !== "t") {
|
|
166
|
-
|
|
167
|
-
"Analytics rollup backfill
|
|
181
|
+
console.info(
|
|
182
|
+
"[db] Deferring historical Analytics rollup backfill; another instance owns the advisory lock",
|
|
168
183
|
);
|
|
184
|
+
return deferMigration();
|
|
169
185
|
}
|
|
170
186
|
for (const statement of ANALYTICS_ROLLUP_BACKFILL_STATEMENTS.postgres) {
|
|
171
187
|
await tx.execute(statement);
|
|
172
188
|
}
|
|
173
189
|
});
|
|
174
|
-
return;
|
|
190
|
+
return result;
|
|
175
191
|
}
|
|
176
192
|
|
|
177
193
|
const statements = ANALYTICS_ROLLUP_BACKFILL_STATEMENTS.sqlite;
|
|
@@ -1556,6 +1572,15 @@ const runAnalyticsMigrations = runMigrations(
|
|
|
1556
1572
|
* swallowed so it can never fail boot.
|
|
1557
1573
|
*/
|
|
1558
1574
|
export default async (nitroApp: any): Promise<void> => {
|
|
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
|
+
);
|
|
1582
|
+
return;
|
|
1583
|
+
}
|
|
1559
1584
|
await runAnalyticsMigrations(nitroApp);
|
|
1560
1585
|
try {
|
|
1561
1586
|
if (await repairPersistedFirstPartyDashboardQueries()) {
|
|
@@ -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
|
+
}
|