@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.
Files changed (152) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/analysis-workspace/SKILL.md +1 -1
  3. package/corpus/templates/analytics/actions/gong-calls.ts +56 -18
  4. package/corpus/templates/analytics/changelog/2026-08-05-analytics-queries-keep-every-table-tenant-scoped.md +6 -0
  5. package/corpus/templates/analytics/changelog/2026-08-05-fixed-sign-in-and-dashboard-requests-failing-when-background.md +6 -0
  6. package/corpus/templates/analytics/changelog/2026-08-05-historical-rollups-stay-complete-while-live-events-arrive.md +6 -0
  7. package/corpus/templates/analytics/server/db/schema.ts +1 -0
  8. package/corpus/templates/analytics/server/lib/dashboard-report.ts +2 -0
  9. package/corpus/templates/analytics/server/lib/emails.ts +51 -0
  10. package/corpus/templates/analytics/server/lib/first-party-analytics-health.ts +8 -12
  11. package/corpus/templates/analytics/server/lib/first-party-analytics-rollups.ts +9 -0
  12. package/corpus/templates/analytics/server/lib/first-party-analytics.ts +262 -13
  13. package/corpus/templates/analytics/server/lib/gong.ts +132 -33
  14. package/corpus/templates/analytics/server/plugins/agent-chat.ts +1 -1
  15. package/corpus/templates/analytics/server/plugins/auth.ts +21 -2
  16. package/corpus/templates/analytics/server/plugins/db.ts +11 -3
  17. package/corpus/templates/analytics/server/plugins/transactional-emails.ts +5 -0
  18. package/corpus/templates/calendar/app/components/calendar/InlineEventPickers.tsx +17 -7
  19. package/corpus/templates/calendar/server/lib/booking-emails.ts +154 -70
  20. package/corpus/templates/calendar/server/lib/emails.ts +167 -0
  21. package/corpus/templates/calendar/server/lib/event-guest-notifications.ts +64 -28
  22. package/corpus/templates/calendar/server/plugins/transactional-emails.ts +5 -0
  23. package/corpus/templates/clips/actions/invite-member.ts +39 -16
  24. package/corpus/templates/clips/app/components/player/share-dialog.tsx +73 -1
  25. package/corpus/templates/clips/app/i18n/en-US.ts +3 -0
  26. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +10 -25
  27. package/corpus/templates/clips/app/routes/share.$shareId.tsx +2 -0
  28. package/corpus/templates/clips/changelog/2026-08-05-activity-is-now-the-default-clips-recording-tab-and-ai-setup.md +6 -0
  29. package/corpus/templates/clips/changelog/2026-08-05-clips-can-copy-a-linked-email-preview-for-public-recordings.md +6 -0
  30. package/corpus/templates/clips/changelog/2026-08-05-preparing-recording-now-uses-a-clean-shadow-free-status-card.md +6 -0
  31. package/corpus/templates/clips/desktop/src/styles.css +0 -3
  32. package/corpus/templates/clips/server/lib/emails.ts +256 -0
  33. package/corpus/templates/clips/server/lib/transactional-email-templates.ts +24 -0
  34. package/corpus/templates/clips/server/plugins/transactional-email-catalog.ts +5 -0
  35. package/corpus/templates/clips/server/routes/api/thumbnail/[recordingId].get.ts +4 -1
  36. package/corpus/templates/clips/shared/email-preview.ts +61 -0
  37. package/corpus/templates/content/server/lib/comment-notifications.ts +57 -26
  38. package/corpus/templates/content/server/lib/emails.ts +70 -0
  39. package/corpus/templates/content/server/plugins/transactional-emails.ts +5 -0
  40. package/corpus/templates/design/actions/import-design-tokens.ts +11 -8
  41. package/corpus/templates/design/actions/index-design-tokens.ts +13 -6
  42. package/corpus/templates/design/changelog/2026-08-05-design-token-imports-classify-typography-dimensions-correctly.md +6 -0
  43. package/corpus/templates/dispatch/app/routes/transactional-email.$appId.$id.tsx +4 -0
  44. package/corpus/templates/dispatch/app/routes/transactional-email.tsx +4 -0
  45. package/corpus/templates/dispatch/changelog/2026-08-05-transactional-email-catalogs-and-delivery-activity-are-available-again.md +6 -0
  46. package/corpus/templates/factory/AGENTS.md +18 -17
  47. package/corpus/templates/factory/actions/get-factory-automation-health.ts +50 -0
  48. package/corpus/templates/factory/actions/get-triage-config.ts +8 -0
  49. package/corpus/templates/factory/actions/save-triage-config.ts +26 -0
  50. package/corpus/templates/factory/app/i18n/en-US.ts +27 -0
  51. package/corpus/templates/factory/app/routes/factory.tsx +376 -206
  52. package/corpus/templates/factory/changelog/2026-08-05-factory-now-keeps-production-automations-running-shows-sched.md +6 -0
  53. package/corpus/templates/factory/server/db/schema.ts +6 -0
  54. package/corpus/templates/factory/server/plugins/factory-migrations.ts +10 -0
  55. package/corpus/templates/factory/server/plugins/factory-scheduler-job.ts +141 -2
  56. package/corpus/templates/forms/app/pages/FormsListPage.tsx +41 -21
  57. package/corpus/templates/forms/changelog/2026-08-05-form-links-can-now-be-cmd-clicked-to-open-in-a-new-tab.md +6 -0
  58. package/corpus/templates/forms/server/lib/emails.ts +52 -0
  59. package/corpus/templates/forms/server/lib/response-email.ts +2 -0
  60. package/corpus/templates/forms/server/plugins/transactional-emails.ts +5 -0
  61. package/corpus/templates/plan/actions/request-plan-access.ts +33 -12
  62. package/corpus/templates/plan/server/lib/comment-notifications.ts +53 -27
  63. package/corpus/templates/plan/server/lib/emails.ts +67 -0
  64. package/corpus/templates/plan/server/plugins/transactional-emails.ts +5 -0
  65. package/corpus/templates/slides/actions/import-from-url.ts +1 -0
  66. package/corpus/templates/slides/app/components/editor/NewDeckReferenceStep.tsx +492 -381
  67. package/corpus/templates/slides/app/components/editor/PromptDialog.tsx +8 -7
  68. package/corpus/templates/slides/app/components/layout/Layout.tsx +27 -23
  69. package/corpus/templates/slides/app/components/onboarding/FirstDeckOnboardingFlow.tsx +313 -0
  70. package/corpus/templates/slides/app/components/ui/command.tsx +1 -0
  71. package/corpus/templates/slides/app/i18n/en-US.ts +8 -2
  72. package/corpus/templates/slides/app/lib/create-deck-generation.ts +305 -0
  73. package/corpus/templates/slides/app/pages/DeckEditor.tsx +12 -12
  74. package/corpus/templates/slides/app/pages/Index.tsx +127 -137
  75. package/corpus/templates/slides/app/root.tsx +8 -0
  76. package/corpus/templates/slides/changelog/2026-08-05-deck-browsing-keeps-all-and-mine-in-the-header-with-a-tighte.md +6 -0
  77. package/corpus/templates/slides/changelog/2026-08-05-new-deck-creation-now-flows-through-a-clean-full-screen-refe.md +6 -0
  78. package/corpus/templates/slides/changelog/2026-08-05-website-imports-now-include-linked-stylesheets-for-more-accu.md +6 -0
  79. package/corpus/templates/slides/server/lib/comment-notifications.ts +44 -21
  80. package/corpus/templates/slides/server/lib/emails.ts +40 -0
  81. package/corpus/templates/slides/server/plugins/transactional-emails.ts +5 -0
  82. package/dist/brand-kit/index.d.ts +1 -0
  83. package/dist/brand-kit/index.js +1 -0
  84. package/dist/brand-kit/tokens.d.ts +91 -0
  85. package/dist/brand-kit/tokens.js +279 -0
  86. package/dist/brand-kit/types.d.ts +25 -0
  87. package/dist/client/AssistantChat.js +7 -7
  88. package/dist/client/onboarding/FirstRunOnboarding.js +26 -2
  89. package/dist/client/onboarding/first-run-registry.d.ts +25 -0
  90. package/dist/client/onboarding/first-run-registry.js +18 -0
  91. package/dist/client/onboarding/index.d.ts +2 -0
  92. package/dist/client/onboarding/index.js +1 -0
  93. package/dist/collab/routes.d.ts +1 -1
  94. package/dist/deploy/build.d.ts +8 -0
  95. package/dist/deploy/build.js +92 -3
  96. package/dist/deploy/workspace-deploy.js +86 -3
  97. package/dist/email-catalog/actions/list-email-activity.d.ts +5 -0
  98. package/dist/email-catalog/actions/list-email-activity.js +19 -0
  99. package/dist/email-catalog/actions/list-email-engagement.d.ts +5 -0
  100. package/dist/email-catalog/actions/list-email-engagement.js +15 -0
  101. package/dist/email-catalog/actions/list-email-log.d.ts +7 -0
  102. package/dist/email-catalog/actions/list-email-log.js +23 -0
  103. package/dist/email-catalog/actions/list-transactional-emails.d.ts +22 -0
  104. package/dist/email-catalog/actions/list-transactional-emails.js +67 -0
  105. package/dist/email-catalog/actions/render-transactional-email-preview.d.ts +9 -0
  106. package/dist/email-catalog/actions/render-transactional-email-preview.js +25 -0
  107. package/dist/email-catalog/authorize.d.ts +1 -0
  108. package/dist/email-catalog/authorize.js +9 -0
  109. package/dist/email-catalog/log.d.ts +60 -0
  110. package/dist/email-catalog/log.js +142 -0
  111. package/dist/email-catalog/provider-metrics.d.ts +34 -0
  112. package/dist/email-catalog/provider-metrics.js +182 -0
  113. package/dist/email-catalog/registry.d.ts +78 -0
  114. package/dist/email-catalog/registry.js +70 -0
  115. package/dist/email-catalog/schema.d.ts +231 -0
  116. package/dist/email-catalog/schema.js +53 -0
  117. package/dist/email-catalog/system-emails.d.ts +13 -0
  118. package/dist/email-catalog/system-emails.js +67 -0
  119. package/dist/jobs/index.d.ts +2 -0
  120. package/dist/jobs/index.js +2 -0
  121. package/dist/jobs/run-history.js +48 -1
  122. package/dist/jobs/scheduler-dispatch.d.ts +7 -0
  123. package/dist/jobs/scheduler-dispatch.js +7 -0
  124. package/dist/jobs/scheduler-health.d.ts +22 -0
  125. package/dist/jobs/scheduler-health.js +172 -0
  126. package/dist/jobs/scheduler.js +67 -2
  127. package/dist/localization/default-messages.d.ts +45 -0
  128. package/dist/localization/default-messages.js +45 -0
  129. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  130. package/dist/notifications/routes.d.ts +3 -3
  131. package/dist/observability/routes.d.ts +5 -5
  132. package/dist/org/handlers.js +9 -1
  133. package/dist/progress/routes.d.ts +1 -1
  134. package/dist/provider-api/actions/custom-provider-registration.d.ts +10 -10
  135. package/dist/provider-api/actions/provider-api.d.ts +8 -8
  136. package/dist/secrets/routes.d.ts +9 -9
  137. package/dist/server/action-discovery.js +22 -0
  138. package/dist/server/agent-chat/recurring-jobs-runtime.d.ts +7 -1
  139. package/dist/server/agent-chat/recurring-jobs-runtime.js +12 -0
  140. package/dist/server/agent-chat-plugin.d.ts +2 -1
  141. package/dist/server/agent-chat-plugin.js +39 -1
  142. package/dist/server/better-auth-instance.js +17 -2
  143. package/dist/server/design-token-utils.d.ts +6 -1
  144. package/dist/server/design-token-utils.js +252 -59
  145. package/dist/server/email.d.ts +11 -0
  146. package/dist/server/email.js +54 -3
  147. package/dist/server/index.d.ts +1 -0
  148. package/dist/server/index.js +1 -0
  149. package/dist/server/realtime-token.d.ts +1 -1
  150. package/dist/server/transcribe-voice.d.ts +1 -1
  151. package/dist/vite/client.js +20 -14
  152. package/package.json +2 -1
package/corpus/README.md CHANGED
@@ -31,4 +31,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
31
31
 
32
32
  ## Generated Counts
33
33
 
34
- - template files: 7644
34
+ - template files: 7678
@@ -59,7 +59,7 @@ operation. The child must read that staged input before making provider calls.
59
59
  not re-page the provider endpoint to recreate the same corpus.
60
60
  - If the named staged input is missing or unreadable, stop with an explicit
61
61
  missing-input error. Do not silently fall back to a full provider sweep.
62
- - For more than 500 Gong records, do not use `gong-calls(exhaustive=true)`, a
62
+ - For 500 or more Gong records, do not use `gong-calls(exhaustive=true)`, a
63
63
  per-call transcript loop, or a single `provider-api-request` `fetchAllPages`
64
64
  call as the analysis itself. If the term is a configured keyword tracker,
65
65
  stage `/calls/extensive` tracker results and reduce them with a Data Program
@@ -203,12 +203,20 @@ function transcriptRowsByCallId(payload: unknown): Map<string, unknown> {
203
203
  return rows;
204
204
  }
205
205
 
206
- async function fetchTranscriptBatch(calls: GongCall[]): Promise<{
206
+ async function fetchTranscriptBatch(
207
+ calls: GongCall[],
208
+ signal?: AbortSignal,
209
+ ): Promise<{
207
210
  payloads: Map<string, unknown>;
208
211
  errors: TranscriptSearchError[];
209
212
  }> {
210
213
  try {
211
- const payload = await getCallTranscripts(calls.map((call) => call.id));
214
+ const payload = signal
215
+ ? await getCallTranscripts(
216
+ calls.map((call) => call.id),
217
+ { signal },
218
+ )
219
+ : await getCallTranscripts(calls.map((call) => call.id));
212
220
  const payloads = transcriptRowsByCallId(payload);
213
221
  const errors: TranscriptSearchError[] = [];
214
222
  for (const call of calls) {
@@ -223,6 +231,7 @@ async function fetchTranscriptBatch(calls: GongCall[]): Promise<{
223
231
  }
224
232
  return { payloads, errors };
225
233
  } catch (err) {
234
+ if (signal?.aborted) throw err;
226
235
  return {
227
236
  payloads: new Map(),
228
237
  errors: calls.map((call) => ({
@@ -235,7 +244,10 @@ async function fetchTranscriptBatch(calls: GongCall[]): Promise<{
235
244
  }
236
245
  }
237
246
 
238
- async function fetchTranscriptBatches(calls: GongCall[]): Promise<
247
+ async function fetchTranscriptBatches(
248
+ calls: GongCall[],
249
+ signal?: AbortSignal,
250
+ ): Promise<
239
251
  Array<{
240
252
  calls: GongCall[];
241
253
  result: Awaited<ReturnType<typeof fetchTranscriptBatch>>;
@@ -257,7 +269,7 @@ async function fetchTranscriptBatches(calls: GongCall[]): Promise<
257
269
  .slice(index, index + TRANSCRIPT_BATCH_CONCURRENCY)
258
270
  .map(async (batch) => ({
259
271
  calls: batch,
260
- result: await fetchTranscriptBatch(batch),
272
+ result: await fetchTranscriptBatch(batch, signal),
261
273
  })),
262
274
  )),
263
275
  );
@@ -270,6 +282,7 @@ async function searchTranscriptEvidence(
270
282
  query: string,
271
283
  scanLimit: number,
272
284
  maxChars: number,
285
+ signal?: AbortSignal,
273
286
  ): Promise<{
274
287
  inspectedCalls: number;
275
288
  matches: TranscriptSearchMatch[];
@@ -284,7 +297,7 @@ async function searchTranscriptEvidence(
284
297
  for (const {
285
298
  calls: batch,
286
299
  result: batchResult,
287
- } of await fetchTranscriptBatches(callsToScan)) {
300
+ } of await fetchTranscriptBatches(callsToScan, signal)) {
288
301
  errors.push(...batchResult.errors);
289
302
 
290
303
  for (const call of batch) {
@@ -420,13 +433,14 @@ async function loadTranscriptEvidence(
420
433
  calls: GongCall[],
421
434
  limit: number,
422
435
  maxChars: number,
436
+ signal?: AbortSignal,
423
437
  ): Promise<TranscriptEvidence[]> {
424
438
  const evidence: TranscriptEvidence[] = [];
425
439
  const callsToLoad = calls.slice(0, limit);
426
440
  for (const {
427
441
  calls: batch,
428
442
  result: batchResult,
429
- } of await fetchTranscriptBatches(callsToLoad)) {
443
+ } of await fetchTranscriptBatches(callsToLoad, signal)) {
430
444
  const errorByCallId = new Map(
431
445
  batchResult.errors.map((error) => [error.callId, error.error]),
432
446
  );
@@ -486,7 +500,7 @@ export default defineAction({
486
500
  // model round trips while still staying well below the model context limit.
487
501
  maxResultChars: 100_000,
488
502
  description:
489
- "Query bounded Gong sales-call evidence. Pass --users for the user list, --transcript for one transcript, or --company for a bounded search by company/domain/person/email. Without --company, the action lists calls in the date window; set exhaustive=true for every page of a small bounded cohort (up to 500 records), not a broad org-wide export. For account-level transcript mention questions, transcriptQuery performs a case-insensitive local scan after batched transcript retrieval and returns coverage counts plus snippets; it is not Gong's server-side keyword search. Use includeTranscripts=true for bounded qualitative context. For broad keyword, tracker, cross-account, or absence-sensitive work, use provider-api-catalog/provider-api-docs, stage the raw Gong API response, then use query-staged-dataset or a Data Program; use provider-corpus-job only when raw transcript bodies are required.",
503
+ "Query bounded Gong sales-call evidence. Pass --users for the user list, --transcript for one transcript, or --company for a bounded search by company/domain/person/email. Without --company, the action lists calls in the date window; set exhaustive=true only for a small bounded cohort of fewer than 500 records, not a broad org-wide export. For account-level transcript mention questions, transcriptQuery performs a case-insensitive local scan after batched transcript retrieval and returns coverage counts plus snippets; it is not Gong's server-side keyword search. Use includeTranscripts=true for bounded qualitative context. For broad keyword, tracker, cross-account, or absence-sensitive work, use provider-api-catalog/provider-api-docs, stage the raw Gong API response, then use query-staged-dataset or a Data Program; use provider-corpus-job only when raw transcript bodies are required.",
490
504
  schema: z.object({
491
505
  users: cliBoolean.optional().describe("Set to true to list Gong users"),
492
506
  transcript: z.string().optional().describe("Call ID to get transcript"),
@@ -553,7 +567,7 @@ export default defineAction({
553
567
  exhaustive: cliBoolean
554
568
  .optional()
555
569
  .describe(
556
- "Return EVERY matching call in the window instead of stopping at `limit`; without company, return every call page for a small bounded cohort of at most 500 records. Larger cohorts fail closed with guidance to use provider-api-request staging and query-staged-dataset or a Data Program. By default this is metadata-only. With includeTranscripts=true, transcripts are fetched in batches and explicit coverage is returned. For broad or absence-sensitive work, prefer the raw Gong API tracker/staging path plus query-staged-dataset or a Data Program; use provider-corpus-job for durable raw-transcript scans. Always bound this with after/before or a small days window.",
570
+ "Return EVERY matching call in the window instead of stopping at `limit`; without company, return every call page only for a small bounded cohort of fewer than 500 records. Cohorts at or above 500 fail closed with guidance to use provider-api-request staging and query-staged-dataset or a Data Program. By default this is metadata-only. With includeTranscripts=true, transcripts are fetched in batches and explicit coverage is returned. For broad or absence-sensitive work, prefer the raw Gong API tracker/staging path plus query-staged-dataset or a Data Program; use provider-corpus-job for durable raw-transcript scans. Always bound this with after/before or a small days window.",
557
571
  ),
558
572
  after: z
559
573
  .string()
@@ -569,12 +583,15 @@ export default defineAction({
569
583
  ),
570
584
  }),
571
585
  http: { method: "GET" },
572
- run: async (args) => {
586
+ run: async (args, ctx) => {
587
+ const requestOptions = ctx?.signal ? { signal: ctx.signal } : undefined;
573
588
  if (args.users) {
574
589
  const users = await getUsers();
575
590
  return { users, total: users.length };
576
591
  } else if (args.transcript) {
577
- const transcript = await getCallTranscript(args.transcript);
592
+ const transcript = requestOptions
593
+ ? await getCallTranscript(args.transcript, requestOptions)
594
+ : await getCallTranscript(args.transcript);
578
595
  const transcriptText = extractTranscriptText(
579
596
  transcript,
580
597
  args.transcriptMaxChars,
@@ -600,6 +617,7 @@ export default defineAction({
600
617
  exhaustive,
601
618
  ...(fromDateTime ? { fromDateTime } : {}),
602
619
  ...(toDateTime ? { toDateTime } : {}),
620
+ ...(requestOptions ?? {}),
603
621
  });
604
622
  const shouldLoadTranscripts = Boolean(args.includeTranscripts);
605
623
  const transcriptLimit = normalizeBoundedInt(
@@ -644,6 +662,7 @@ export default defineAction({
644
662
  transcriptQuery,
645
663
  transcriptScanLimit,
646
664
  transcriptSearchMaxChars,
665
+ requestOptions?.signal,
647
666
  )
648
667
  : undefined;
649
668
  const transcripts = shouldLoadTranscripts
@@ -651,6 +670,7 @@ export default defineAction({
651
670
  result.calls,
652
671
  transcriptLimit,
653
672
  boundedTranscriptExcerptMaxChars,
673
+ requestOptions?.signal,
654
674
  )
655
675
  : undefined;
656
676
  const transcriptErrors =
@@ -726,14 +746,30 @@ export default defineAction({
726
746
  new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
727
747
  const toDateTime = normalizeGongDate(args.before, "end");
728
748
  const result = exhaustive
729
- ? await getAllCalls({
730
- fromDateTime,
731
- ...(toDateTime ? { toDateTime } : {}),
732
- })
733
- : await getCalls({
734
- fromDateTime,
735
- ...(toDateTime ? { toDateTime } : {}),
736
- });
749
+ ? requestOptions
750
+ ? await getAllCalls(
751
+ {
752
+ fromDateTime,
753
+ ...(toDateTime ? { toDateTime } : {}),
754
+ },
755
+ requestOptions,
756
+ )
757
+ : await getAllCalls({
758
+ fromDateTime,
759
+ ...(toDateTime ? { toDateTime } : {}),
760
+ })
761
+ : requestOptions
762
+ ? await getCalls(
763
+ {
764
+ fromDateTime,
765
+ ...(toDateTime ? { toDateTime } : {}),
766
+ },
767
+ requestOptions,
768
+ )
769
+ : await getCalls({
770
+ fromDateTime,
771
+ ...(toDateTime ? { toDateTime } : {}),
772
+ });
737
773
  const limited = limitGongCalls(result.calls, limit);
738
774
  const returnedCalls = exhaustive ? result.calls : limited.calls;
739
775
  const truncated =
@@ -776,6 +812,7 @@ export default defineAction({
776
812
  transcriptQuery,
777
813
  transcriptScanLimit,
778
814
  transcriptSearchMaxChars,
815
+ requestOptions?.signal,
779
816
  )
780
817
  : undefined;
781
818
  const transcripts = shouldLoadTranscripts
@@ -783,6 +820,7 @@ export default defineAction({
783
820
  returnedCalls,
784
821
  transcriptLimit,
785
822
  boundedTranscriptExcerptMaxChars,
823
+ requestOptions?.signal,
786
824
  )
787
825
  : undefined;
788
826
 
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: security
3
+ date: 2026-08-05
4
+ ---
5
+
6
+ Analytics dashboard queries keep every table tenant-scoped.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-08-05
4
+ ---
5
+
6
+ Fixed sign-in and dashboard requests failing when background jobs start
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-08-05
4
+ ---
5
+
6
+ Historical analytics rollups stay complete while live events arrive.
@@ -299,6 +299,7 @@ export const analyticsQueryPressureDaily = table(
299
299
  tenantKey: text("tenant_key").notNull(),
300
300
  ownerEmail: text("owner_email").notNull(),
301
301
  orgId: text("org_id"),
302
+ /** UTC day bucket; lastSeenAt carries the timestamp for trailing-window reads. */
302
303
  eventDate: text("event_date").notNull(),
303
304
  queryClass: text("query_class").notNull(),
304
305
  slowQueryCount: integer("slow_query_count").notNull().default(0),
@@ -18,6 +18,7 @@ import {
18
18
  type DashboardReportCaptureOutcome,
19
19
  type DashboardReportSubscription,
20
20
  } from "./dashboard-report-subscriptions";
21
+ import { ANALYTICS_DASHBOARD_REPORT_EMAIL_ID } from "./emails";
21
22
 
22
23
  const DATE_FILTER_TYPES: ReadonlySet<FilterType> = new Set([
23
24
  "date",
@@ -326,6 +327,7 @@ export async function sendDashboardReportSubscription(
326
327
  ? { attachments: rendered.attachments }
327
328
  : {}),
328
329
  timeoutMs: emailTimeoutMs,
330
+ templateId: ANALYTICS_DASHBOARD_REPORT_EMAIL_ID,
329
331
  });
330
332
  }
331
333
 
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Catalog entries for the transactional emails Analytics 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
+ import { emailStrong, renderEmail } from "@agent-native/core/server";
10
+
11
+ export const ANALYTICS_DASHBOARD_REPORT_EMAIL_ID = "analytics.dashboard-report";
12
+
13
+ let registered = false;
14
+
15
+ export function registerAnalyticsEmails(): void {
16
+ if (registered) return;
17
+ registered = true;
18
+
19
+ defineTransactionalEmail({
20
+ id: ANALYTICS_DASHBOARD_REPORT_EMAIL_ID,
21
+ name: "Scheduled dashboard report",
22
+ trigger:
23
+ "A dashboard report subscription comes due and at least one panel produced usable data. A run where every queried panel failed throws instead of mailing, and a degraded run is skipped when the subscription asks for complete reports only.",
24
+ recipientLabel: "Subscription recipients",
25
+ recipient:
26
+ "The normalized recipient list stored on the subscription. One email per address, all carrying the same rendered snapshot.",
27
+ senderLabel: "Default sender",
28
+ sender:
29
+ "The configured default sender. This call site sets no `from`, `fromName`, `replyTo`, or `appSender`.",
30
+ // The real renderer runs every panel query and rasterizes charts, so it
31
+ // cannot back a preview that must stay offline. This shows the frame a
32
+ // recipient sees; the panel body is whatever that run produced.
33
+ preview: () => ({
34
+ subject: "Daily dashboard: Growth overview — 3/4/2025",
35
+ ...renderEmail({
36
+ preheader: "Your daily Growth overview report is ready.",
37
+ heading: "Daily dashboard: Growth overview",
38
+ paragraphs: [
39
+ `This report renders each panel of ${emailStrong("Growth overview")} as of the run time, with charts drawn server-side and attached inline.`,
40
+ "Panels that could not be queried are called out in place rather than dropped, so a partial report never reads as a complete one.",
41
+ ],
42
+ cta: {
43
+ label: "Open dashboard",
44
+ url: "https://example.com/dashboards/dash_sample",
45
+ },
46
+ footer:
47
+ "You received this because you are on the recipient list for this dashboard report.",
48
+ }),
49
+ }),
50
+ });
51
+ }
@@ -73,10 +73,6 @@ function nowIso(): string {
73
73
  return new Date().toISOString();
74
74
  }
75
75
 
76
- function todayIsoDate(): string {
77
- return nowIso().slice(0, 10);
78
- }
79
-
80
76
  function tenantKey(scope: AnalyticsScope): string {
81
77
  return scope.orgId ? `org:${scope.orgId}` : `user:${scope.userEmail}`;
82
78
  }
@@ -152,7 +148,8 @@ export async function recordFirstPartyAnalyticsQueryPressure(
152
148
  if (!shouldRecordPressure(event)) return;
153
149
 
154
150
  const durationMs = normalizedDurationMs(event.durationMs);
155
- const eventDate = todayIsoDate();
151
+ const eventTimestamp = nowIso();
152
+ const eventDate = eventTimestamp.slice(0, 10);
156
153
  const key = tenantKey(scope);
157
154
  const table = schema.analyticsQueryPressureDaily;
158
155
  const db = getDb() as any;
@@ -174,7 +171,7 @@ export async function recordFirstPartyAnalyticsQueryPressure(
174
171
  errorCount: event.outcome === "error" ? 1 : 0,
175
172
  totalDurationMs: durationMs,
176
173
  maxDurationMs: durationMs,
177
- lastSeenAt: nowIso(),
174
+ lastSeenAt: eventTimestamp,
178
175
  })
179
176
  .onConflictDoUpdate({
180
177
  target: [table.tenantKey, table.eventDate, table.queryClass],
@@ -184,7 +181,7 @@ export async function recordFirstPartyAnalyticsQueryPressure(
184
181
  errorCount: sql`${table.errorCount} + ${event.outcome === "error" ? 1 : 0}`,
185
182
  totalDurationMs: sql`${table.totalDurationMs} + ${durationMs}`,
186
183
  maxDurationMs: sql`CASE WHEN ${table.maxDurationMs} > ${durationMs} THEN ${table.maxDurationMs} ELSE ${durationMs} END`,
187
- lastSeenAt: nowIso(),
184
+ lastSeenAt: eventTimestamp,
188
185
  },
189
186
  });
190
187
  }
@@ -250,9 +247,8 @@ export async function getFirstPartyAnalyticsHealth(
250
247
  ): Promise<FirstPartyAnalyticsHealth> {
251
248
  const now = new Date();
252
249
  const today = now.toISOString().slice(0, 10);
253
- const since24h = new Date(now.getTime() - 86_400_000)
254
- .toISOString()
255
- .slice(0, 10);
250
+ const nowTimestamp = now.toISOString();
251
+ const since24h = new Date(now.getTime() - 86_400_000).toISOString();
256
252
  const keys = scope.orgId
257
253
  ? [`org:${scope.orgId}`, `user:${scope.userEmail}`]
258
254
  : [`user:${scope.userEmail}`];
@@ -284,8 +280,8 @@ export async function getFirstPartyAnalyticsHealth(
284
280
  .where(
285
281
  and(
286
282
  inArray(pressure.tenantKey, keys),
287
- gte(pressure.eventDate, since24h),
288
- lte(pressure.eventDate, today),
283
+ gte(pressure.lastSeenAt, since24h),
284
+ lte(pressure.lastSeenAt, nowTimestamp),
289
285
  ),
290
286
  ),
291
287
  bigQueryStatus(scope),
@@ -23,6 +23,15 @@ export interface FirstPartyAnalyticsRollupResult {
23
23
  userDayCount: number;
24
24
  }
25
25
 
26
+ /**
27
+ * Shared with the historical backfill so live ingest and migration work hold
28
+ * the same transaction-scoped Postgres lock while they update rollups.
29
+ */
30
+ export const FIRST_PARTY_ANALYTICS_ROLLUP_LOCK_KEY =
31
+ "agent-native:analytics-rollup-backfill";
32
+ export const FIRST_PARTY_ANALYTICS_ROLLUP_LOCK_SQL =
33
+ "SELECT pg_advisory_xact_lock(hashtextextended(?, 0::bigint))";
34
+
26
35
  interface DailyRollupRow {
27
36
  id: string;
28
37
  tenantKey: string;