@agent-native/core 0.141.5 → 0.141.7

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 (235) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +3 -14
  3. package/corpus/templates/analytics/.agents/skills/data-querying/SKILL.md +20 -8
  4. package/corpus/templates/analytics/.agents/skills/prometheus/SKILL.md +5 -5
  5. package/corpus/templates/analytics/AGENTS.md +2 -2
  6. package/corpus/templates/analytics/actions/compose-dashboard.ts +1 -1
  7. package/corpus/templates/analytics/actions/get-first-party-analytics-health.ts +15 -1
  8. package/corpus/templates/analytics/actions/migrate-first-party-analytics-to-bigquery.ts +135 -0
  9. package/corpus/templates/analytics/actions/navigate.ts +1 -1
  10. package/corpus/templates/analytics/actions/query-agent-native-analytics.ts +2 -2
  11. package/corpus/templates/analytics/actions/update-dashboard.ts +0 -1
  12. package/corpus/templates/analytics/actions/view-screen.ts +0 -20
  13. package/corpus/templates/analytics/app/components/layout/Header.tsx +0 -1
  14. package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +5 -5
  15. package/corpus/templates/analytics/app/i18n-data.ts +330 -90
  16. package/corpus/templates/analytics/app/pages/Agents.tsx +0 -3
  17. package/corpus/templates/analytics/app/pages/DataSources.tsx +66 -30
  18. package/corpus/templates/analytics/app/pages/Settings.tsx +22 -43
  19. package/corpus/templates/analytics/app/pages/settings/settings-search.ts +23 -20
  20. package/corpus/templates/analytics/app/routes/extensions._index.tsx +1 -1
  21. package/corpus/templates/analytics/app/routes/settings.$.tsx +1 -0
  22. package/corpus/templates/analytics/app/routes/team.tsx +1 -1
  23. package/corpus/templates/analytics/changelog/2026-08-05-historical-analytics-rollups-now-repair-automatically-after-.md +6 -0
  24. package/corpus/templates/analytics/changelog/2026-08-05-settings-no-longer-includes-an-unnecessary-about-section.md +6 -0
  25. package/corpus/templates/analytics/docs/schemas/first-party-analytics.md +1 -1
  26. package/corpus/templates/analytics/netlify.toml +4 -0
  27. package/corpus/templates/analytics/scripts/emit-netlify-dashboard-report-cron.ts +109 -0
  28. package/corpus/templates/analytics/scripts/first-party-analytics-bigquery.sql +104 -0
  29. package/corpus/templates/analytics/server/jobs/analytics-rollup-backfill.ts +342 -0
  30. package/corpus/templates/analytics/server/lib/agent-chat-plan-mode.ts +0 -2
  31. package/corpus/templates/analytics/server/lib/analytics-rollup-lock.ts +8 -0
  32. package/corpus/templates/analytics/server/lib/dashboard-catalog.ts +11 -29
  33. package/corpus/templates/analytics/server/lib/dashboards-store.ts +55 -0
  34. package/corpus/templates/analytics/server/lib/first-party-analytics-backend.ts +1058 -0
  35. package/corpus/templates/analytics/server/lib/first-party-analytics-health.ts +151 -46
  36. package/corpus/templates/analytics/server/lib/first-party-analytics-rollups.ts +15 -9
  37. package/corpus/templates/analytics/server/lib/first-party-analytics.ts +59 -17
  38. package/corpus/templates/analytics/server/lib/real-data-actions.ts +1 -3
  39. package/corpus/templates/analytics/server/plugins/agent-chat.ts +2 -4
  40. package/corpus/templates/analytics/server/plugins/analytics-rollup-backfill-jobs.ts +61 -0
  41. package/corpus/templates/analytics/server/plugins/db.ts +58 -187
  42. package/corpus/templates/analytics/server/routes/api/analytics-rollups/backfill.post.ts +53 -0
  43. package/corpus/templates/assets/.agents/skills/asset-generation/SKILL.md +8 -6
  44. package/corpus/templates/assets/.agents/skills/library-management/SKILL.md +16 -1
  45. package/corpus/templates/assets/AGENTS.md +1 -0
  46. package/corpus/templates/assets/actions/_helpers.ts +12 -0
  47. package/corpus/templates/assets/actions/generate-image.ts +9 -3
  48. package/corpus/templates/assets/actions/import-style-from-url.ts +115 -0
  49. package/corpus/templates/assets/app/hooks/use-navigation-state.ts +3 -1
  50. package/corpus/templates/assets/app/routes/extensions._index.tsx +1 -1
  51. package/corpus/templates/assets/app/routes/settings.$.tsx +1 -0
  52. package/corpus/templates/assets/app/routes/settings.tsx +5 -0
  53. package/corpus/templates/assets/app/routes/team.tsx +1 -1
  54. package/corpus/templates/assets/changelog/2026-08-05-brand-kits-can-import-a-website-s-hydrated-visual-language-a.md +6 -0
  55. package/corpus/templates/assets/changelog/2026-08-05-generated-images-now-hand-off-embeddable-previews-to-other-w.md +6 -0
  56. package/corpus/templates/assets/server/lib/generation.ts +58 -0
  57. package/corpus/templates/assets/shared/api.ts +12 -0
  58. package/corpus/templates/brain/app/routes/extensions._index.tsx +1 -1
  59. package/corpus/templates/brain/app/routes/team.tsx +1 -1
  60. package/corpus/templates/brain/app/routes.ts +1 -1
  61. package/corpus/templates/calendar/app/components/layout/AppLayout.tsx +3 -1
  62. package/corpus/templates/calendar/app/routes/_app.extensions._index.tsx +1 -1
  63. package/corpus/templates/calendar/app/routes/_app.settings.$.tsx +1 -0
  64. package/corpus/templates/calendar/app/routes/_app.team.tsx +1 -1
  65. package/corpus/templates/chat/app/components/layout/Layout.tsx +1 -1
  66. package/corpus/templates/chat/app/hooks/use-navigation-state.ts +1 -1
  67. package/corpus/templates/chat/app/root.tsx +1 -1
  68. package/corpus/templates/chat/app/routes/agent.tsx +7 -7
  69. package/corpus/templates/chat/app/routes/extensions._index.tsx +1 -1
  70. package/corpus/templates/chat/app/routes/settings.$.tsx +1 -0
  71. package/corpus/templates/chat/app/routes/team.tsx +1 -1
  72. package/corpus/templates/clips/app/components/library/library-layout.tsx +1 -1
  73. package/corpus/templates/clips/app/components/player/transcript-panel.tsx +1 -1
  74. package/corpus/templates/clips/app/components/recorder/storage-setup-card.tsx +1 -1
  75. package/corpus/templates/clips/app/routes/_app.extensions._index.tsx +1 -1
  76. package/corpus/templates/clips/app/routes/_app.settings.$.tsx +1 -0
  77. package/corpus/templates/clips/changelog/2026-08-05-clips-overlays-now-use-clean-surfaces-without-clipped-shadow.md +6 -0
  78. package/corpus/templates/clips/desktop/src/overlays/meeting-notification.tsx +2 -2
  79. package/corpus/templates/clips/desktop/src/styles.css +3 -54
  80. package/corpus/templates/clips/desktop/src-tauri/src/notifications.rs +3 -3
  81. package/corpus/templates/content/app/routes/_app.extensions._index.tsx +1 -1
  82. package/corpus/templates/content/app/routes/_app.settings.$.tsx +1 -0
  83. package/corpus/templates/content/app/routes/_app.team.tsx +1 -1
  84. package/corpus/templates/content/e2e/global-setup.ts +1 -1
  85. package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +6 -1
  86. package/corpus/templates/design/actions/analyze-brand-assets.ts +7 -7
  87. package/corpus/templates/design/actions/get-design-system.ts +3 -1
  88. package/corpus/templates/design/actions/import-from-url.ts +6 -5
  89. package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +2 -2
  90. package/corpus/templates/design/app/routes/extensions._index.tsx +1 -1
  91. package/corpus/templates/design/app/routes/settings.$.tsx +1 -0
  92. package/corpus/templates/design/app/routes/team.tsx +1 -1
  93. package/corpus/templates/design/changelog/2026-08-05-missing-design-systems-now-return-a-clear-not-found-response.md +6 -0
  94. package/corpus/templates/design/changelog/2026-08-05-website-url-imports-now-capture-hydrated-browser-computed-de.md +6 -0
  95. package/corpus/templates/design/e2e/global-setup.ts +1 -1
  96. package/corpus/templates/dispatch/app/hooks/use-navigation-state.ts +1 -1
  97. package/corpus/templates/dispatch/app/routes/settings.$.tsx +1 -0
  98. package/corpus/templates/dispatch/app/routes/team.tsx +1 -1
  99. package/corpus/templates/factory/app/hooks/use-navigation-state.ts +1 -1
  100. package/corpus/templates/factory/app/routes/extensions._index.tsx +1 -1
  101. package/corpus/templates/factory/app/routes/settings.$.tsx +1 -0
  102. package/corpus/templates/factory/app/routes/team.tsx +1 -1
  103. package/corpus/templates/forms/app/routes/_app.extensions._index.tsx +1 -1
  104. package/corpus/templates/forms/app/routes/_app.settings.$.tsx +1 -0
  105. package/corpus/templates/forms/app/routes/_app.team.tsx +1 -1
  106. package/corpus/templates/forms/shared/navigation.ts +1 -1
  107. package/corpus/templates/macros/app/routes/extensions._index.tsx +1 -1
  108. package/corpus/templates/macros/app/routes/settings.$.tsx +1 -0
  109. package/corpus/templates/macros/app/routes/team.tsx +1 -1
  110. package/corpus/templates/mail/app/routes/extensions._index.tsx +1 -1
  111. package/corpus/templates/mail/app/routes/settings.$.tsx +1 -0
  112. package/corpus/templates/plan/app/hooks/use-navigation-state.ts +1 -1
  113. package/corpus/templates/plan/app/routes/extensions._index.tsx +1 -1
  114. package/corpus/templates/plan/app/routes/settings.$.tsx +1 -0
  115. package/corpus/templates/plan/app/routes/team.tsx +1 -1
  116. package/corpus/templates/slides/.agents/skills/design-systems/SKILL.md +3 -1
  117. package/corpus/templates/slides/actions/analyze-brand-assets.ts +7 -7
  118. package/corpus/templates/slides/actions/import-from-url.ts +6 -6
  119. package/corpus/templates/slides/app/components/deck/DeckCard.tsx +2 -2
  120. package/corpus/templates/slides/app/components/design-system/DesignSystemSetup.tsx +1 -1
  121. package/corpus/templates/slides/app/components/editor/PromptDialog.tsx +3 -0
  122. package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +1 -1
  123. package/corpus/templates/slides/app/components/onboarding/FirstDeckOnboardingFlow.tsx +9 -3
  124. package/corpus/templates/slides/app/context/DeckContext.tsx +71 -20
  125. package/corpus/templates/slides/app/global.css +6 -0
  126. package/corpus/templates/slides/app/i18n/en-US.ts +5 -5
  127. package/corpus/templates/slides/app/lib/create-deck-generation.ts +9 -5
  128. package/corpus/templates/slides/app/pages/DeckEditor.tsx +17 -0
  129. package/corpus/templates/slides/app/pages/Index.tsx +16 -5
  130. package/corpus/templates/slides/app/routes/extensions._index.tsx +1 -1
  131. package/corpus/templates/slides/app/routes/settings.$.tsx +1 -0
  132. package/corpus/templates/slides/app/routes/team.tsx +1 -1
  133. package/corpus/templates/slides/changelog/2026-08-05-failed-deck-creation-and-reference-uploads-now-recover-clean.md +6 -0
  134. package/corpus/templates/slides/changelog/2026-08-05-new-presentation-prompts-now-clearly-describe-presentation-g.md +6 -0
  135. package/corpus/templates/slides/changelog/2026-08-05-slides-recover-completed-generation-results-after-a-sync-hic.md +6 -0
  136. package/corpus/templates/slides/changelog/2026-08-05-website-url-imports-now-capture-hydrated-browser-computed-st.md +6 -0
  137. package/corpus/templates/tasks/app/components/layout/Header.tsx +3 -1
  138. package/corpus/templates/tasks/app/routes/extensions._index.tsx +1 -1
  139. package/corpus/templates/tasks/app/routes/settings.$.tsx +1 -0
  140. package/dist/agent/engine/ai-sdk-engine.js +49 -3
  141. package/dist/agent/production-agent.d.ts +1 -0
  142. package/dist/agent/production-agent.js +8 -0
  143. package/dist/agent/types.d.ts +7 -0
  144. package/dist/client/AssistantChat.js +1 -1
  145. package/dist/client/agent-chat-adapter.d.ts +1 -1
  146. package/dist/client/agent-chat-adapter.js +30 -3
  147. package/dist/client/agent-page/AgentWorkspaceContent.d.ts +1 -1
  148. package/dist/client/agent-page/AgentWorkspaceContent.js +13 -13
  149. package/dist/client/chat/run-recovery.d.ts +2 -1
  150. package/dist/client/chat/run-recovery.js +14 -3
  151. package/dist/client/embed-auth.js +6 -2
  152. package/dist/client/error-format.d.ts +1 -0
  153. package/dist/client/error-format.js +16 -2
  154. package/dist/client/integrations/IntegrationsPanel.js +40 -6
  155. package/dist/client/org/OrgSwitcher.js +2 -2
  156. package/dist/client/org/TeamPage.js +92 -72
  157. package/dist/client/require-session.d.ts +1 -1
  158. package/dist/client/require-session.js +3 -3
  159. package/dist/client/resources/use-mcp-servers.d.ts +5 -0
  160. package/dist/client/resources/use-mcp-servers.js +36 -0
  161. package/dist/client/settings/AccountSettingsCard.js +40 -35
  162. package/dist/client/settings/AutomationsSection.js +1 -1
  163. package/dist/client/settings/DemoModeSection.d.ts +3 -1
  164. package/dist/client/settings/DemoModeSection.js +4 -1
  165. package/dist/client/settings/ExtensionsSettingsContent.js +1 -1
  166. package/dist/client/settings/SchedulingTimezoneField.d.ts +3 -1
  167. package/dist/client/settings/SchedulingTimezoneField.js +9 -5
  168. package/dist/client/settings/SettingsPanel.d.ts +4 -1
  169. package/dist/client/settings/SettingsPanel.js +21 -21
  170. package/dist/client/settings/SettingsTabsPage.d.ts +2 -2
  171. package/dist/client/settings/SettingsTabsPage.js +98 -45
  172. package/dist/client/settings/VoiceTranscriptionSection.js +6 -1
  173. package/dist/client/settings/useSettingsPanelController.js +47 -8
  174. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  175. package/dist/ingestion/website.d.ts +30 -0
  176. package/dist/localization/default-messages.d.ts +5 -0
  177. package/dist/localization/default-messages.js +5 -0
  178. package/dist/mcp-client/oauth-routes.js +1 -1
  179. package/dist/mcp-client/routes.d.ts +1 -0
  180. package/dist/mcp-client/routes.js +49 -0
  181. package/dist/navigation/index.js +16 -5
  182. package/dist/observability/routes.d.ts +3 -3
  183. package/dist/provider-api/actions/custom-provider-registration.d.ts +13 -13
  184. package/dist/provider-api/actions/provider-api.d.ts +11 -11
  185. package/dist/scripts/call-agent.js +11 -0
  186. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  187. package/dist/server/auth.js +9 -7
  188. package/dist/server/identity-sso.js +2 -1
  189. package/dist/server/onboarding-html.js +57 -34
  190. package/dist/server/realtime-token.d.ts +1 -1
  191. package/dist/server/transcribe-voice.d.ts +1 -1
  192. package/dist/server/workspace-provider-oauth.js +1 -1
  193. package/dist/setup-connections/onboarding.js +1 -1
  194. package/dist/shared/index.d.ts +1 -1
  195. package/dist/shared/index.js +1 -1
  196. package/dist/shared/sign-in-journey.d.ts +5 -2
  197. package/dist/shared/sign-in-journey.js +10 -4
  198. package/dist/templates/chat/app/components/layout/Layout.tsx +1 -1
  199. package/dist/templates/chat/app/hooks/use-navigation-state.ts +1 -1
  200. package/dist/templates/chat/app/root.tsx +1 -1
  201. package/dist/templates/chat/app/routes/agent.tsx +7 -7
  202. package/dist/templates/chat/app/routes/extensions._index.tsx +1 -1
  203. package/dist/templates/chat/app/routes/settings.$.tsx +1 -0
  204. package/dist/templates/chat/app/routes/team.tsx +1 -1
  205. package/dist/templates/workspace-core/.agents/skills/address-feedback/SKILL.md +37 -6
  206. package/dist/templates/workspace-core/.agents/skills/authentication/SKILL.md +4 -2
  207. package/docs/content/authentication.mdx +4 -5
  208. package/docs/content/template-analytics-developers.mdx +1 -1
  209. package/docs/content/template-calendar-developers.mdx +1 -1
  210. package/docs/content/template-chat-developers.mdx +2 -2
  211. package/docs/content/template-content-developers.mdx +1 -1
  212. package/docs/content/template-mail-developers.mdx +1 -1
  213. package/docs/content/template-plan-developers.mdx +1 -1
  214. package/docs/content/toolkit-command-navigation.mdx +6 -6
  215. package/docs/content/toolkit-org-team.mdx +1 -1
  216. package/docs/content/toolkit-settings.mdx +8 -8
  217. package/docs/content/toolkit-setup-connections.mdx +1 -1
  218. package/package.json +1 -1
  219. package/src/templates/chat/app/components/layout/Layout.tsx +1 -1
  220. package/src/templates/chat/app/hooks/use-navigation-state.ts +1 -1
  221. package/src/templates/chat/app/root.tsx +1 -1
  222. package/src/templates/chat/app/routes/agent.tsx +7 -7
  223. package/src/templates/chat/app/routes/extensions._index.tsx +1 -1
  224. package/src/templates/chat/app/routes/settings.$.tsx +1 -0
  225. package/src/templates/chat/app/routes/team.tsx +1 -1
  226. package/src/templates/workspace-core/.agents/skills/address-feedback/SKILL.md +37 -6
  227. package/src/templates/workspace-core/.agents/skills/authentication/SKILL.md +4 -2
  228. package/corpus/templates/analytics/.agents/skills/dashboard-management/references/template-catalog-and-demo.md +0 -62
  229. package/corpus/templates/analytics/actions/install-dashboard-template.ts +0 -369
  230. package/corpus/templates/analytics/actions/list-dashboard-templates.ts +0 -47
  231. package/corpus/templates/analytics/actions/list-native-v2-dashboard-templates.ts +0 -34
  232. package/corpus/templates/analytics/app/routes/catalog.tsx +0 -240
  233. package/corpus/templates/analytics/changelog/2026-08-05-historical-analytics-rollups-stay-complete-and-atomic.md +0 -6
  234. package/corpus/templates/analytics/changelog/2026-08-05-historical-rollups-stay-complete-while-live-events-arrive.md +0 -6
  235. package/src/templates/default/actions/navigate.spec.ts +0 -53
@@ -0,0 +1,342 @@
1
+ import { getDbExec, getDialect, isPostgres } from "@agent-native/core/db";
2
+
3
+ import { FIRST_PARTY_ANALYTICS_ROLLUP_LOCK_KEY } from "../lib/analytics-rollup-lock";
4
+
5
+ const BACKFILL_STATE_ID = "historical-v1";
6
+ const BACKFILL_LEASE_MINUTES = 15;
7
+
8
+ const POSTGRES_BACKFILL_STATEMENTS = [
9
+ `
10
+ INSERT INTO analytics_event_daily_rollups (
11
+ id, tenant_key, owner_email, org_id, event_date, event_name,
12
+ app, template, event_count
13
+ )
14
+ SELECT
15
+ md5(random()::text || clock_timestamp()::text), tenant_key,
16
+ MIN(owner_email), MIN(org_id), event_date, event_name, app, template,
17
+ COUNT(*)::INTEGER
18
+ FROM (
19
+ SELECT
20
+ CASE
21
+ WHEN org_id IS NOT NULL AND org_id <> '' THEN 'org:' || org_id
22
+ ELSE 'user:' || owner_email
23
+ END AS tenant_key,
24
+ owner_email,
25
+ org_id,
26
+ COALESCE(NULLIF(event_date, ''), substr(timestamp, 1, 10)) AS event_date,
27
+ event_name,
28
+ COALESCE(app, '') AS app,
29
+ COALESCE(template, '') AS template
30
+ FROM analytics_events
31
+ ) AS historical_events
32
+ WHERE event_date <> ''
33
+ GROUP BY tenant_key, event_date, event_name, app, template
34
+ ON CONFLICT (tenant_key, event_date, event_name, app, template)
35
+ DO UPDATE SET event_count = GREATEST(
36
+ analytics_event_daily_rollups.event_count,
37
+ EXCLUDED.event_count
38
+ )
39
+ `,
40
+ `
41
+ INSERT INTO analytics_user_days (
42
+ id, tenant_key, owner_email, org_id, event_date, user_key
43
+ )
44
+ SELECT
45
+ md5(random()::text || clock_timestamp()::text), tenant_key,
46
+ owner_email, org_id, event_date, user_key
47
+ FROM (
48
+ SELECT DISTINCT
49
+ CASE
50
+ WHEN org_id IS NOT NULL AND org_id <> '' THEN 'org:' || org_id
51
+ ELSE 'user:' || owner_email
52
+ END AS tenant_key,
53
+ owner_email,
54
+ org_id,
55
+ COALESCE(NULLIF(event_date, ''), substr(timestamp, 1, 10)) AS event_date,
56
+ COALESCE(
57
+ NULLIF(user_key, ''),
58
+ NULLIF(user_id, ''),
59
+ NULLIF(anonymous_id, '')
60
+ ) AS user_key
61
+ FROM analytics_events
62
+ WHERE COALESCE(
63
+ NULLIF(user_key, ''),
64
+ NULLIF(user_id, ''),
65
+ NULLIF(anonymous_id, '')
66
+ ) IS NOT NULL
67
+ ) AS historical_user_days
68
+ WHERE event_date <> '' AND user_key <> ''
69
+ ON CONFLICT (tenant_key, event_date, user_key) DO NOTHING
70
+ `,
71
+ ] as const;
72
+
73
+ const SQLITE_BACKFILL_STATEMENTS = [
74
+ `
75
+ INSERT INTO analytics_event_daily_rollups (
76
+ id, tenant_key, owner_email, org_id, event_date, event_name,
77
+ app, template, event_count
78
+ )
79
+ SELECT
80
+ lower(hex(randomblob(16))), tenant_key, MIN(owner_email), MIN(org_id),
81
+ event_date, event_name, app, template, COUNT(*)
82
+ FROM (
83
+ SELECT
84
+ CASE
85
+ WHEN org_id IS NOT NULL AND org_id <> '' THEN 'org:' || org_id
86
+ ELSE 'user:' || owner_email
87
+ END AS tenant_key,
88
+ owner_email,
89
+ org_id,
90
+ COALESCE(NULLIF(event_date, ''), substr(timestamp, 1, 10)) AS event_date,
91
+ event_name,
92
+ COALESCE(app, '') AS app,
93
+ COALESCE(template, '') AS template
94
+ FROM analytics_events
95
+ ) AS historical_events
96
+ WHERE event_date <> ''
97
+ GROUP BY tenant_key, event_date, event_name, app, template
98
+ ON CONFLICT (tenant_key, event_date, event_name, app, template)
99
+ DO UPDATE SET event_count = MAX(
100
+ analytics_event_daily_rollups.event_count,
101
+ excluded.event_count
102
+ )
103
+ `,
104
+ `
105
+ INSERT INTO analytics_user_days (
106
+ id, tenant_key, owner_email, org_id, event_date, user_key
107
+ )
108
+ SELECT
109
+ lower(hex(randomblob(16))), tenant_key, owner_email, org_id,
110
+ event_date, user_key
111
+ FROM (
112
+ SELECT DISTINCT
113
+ CASE
114
+ WHEN org_id IS NOT NULL AND org_id <> '' THEN 'org:' || org_id
115
+ ELSE 'user:' || owner_email
116
+ END AS tenant_key,
117
+ owner_email,
118
+ org_id,
119
+ COALESCE(NULLIF(event_date, ''), substr(timestamp, 1, 10)) AS event_date,
120
+ COALESCE(
121
+ NULLIF(user_key, ''),
122
+ NULLIF(user_id, ''),
123
+ NULLIF(anonymous_id, '')
124
+ ) AS user_key
125
+ FROM analytics_events
126
+ WHERE COALESCE(
127
+ NULLIF(user_key, ''),
128
+ NULLIF(user_id, ''),
129
+ NULLIF(anonymous_id, '')
130
+ ) IS NOT NULL
131
+ ) AS historical_user_days
132
+ WHERE event_date <> '' AND user_key <> ''
133
+ ON CONFLICT (tenant_key, event_date, user_key) DO NOTHING
134
+ `,
135
+ ] as const;
136
+
137
+ type BackfillStatus =
138
+ | "completed"
139
+ | "already-complete"
140
+ | "skipped-lock"
141
+ | "already-running";
142
+
143
+ export interface AnalyticsRollupBackfillResult {
144
+ status: BackfillStatus;
145
+ remaining: number;
146
+ }
147
+
148
+ let running = false;
149
+
150
+ function nowSql(): string {
151
+ return isPostgres() ? "now()::text" : "datetime('now')";
152
+ }
153
+
154
+ function ensureStateSql(): string {
155
+ return isPostgres()
156
+ ? `INSERT INTO analytics_rollup_backfill_state (id, status, updated_at)
157
+ VALUES (?, 'pending', ${nowSql()})
158
+ ON CONFLICT (id) DO NOTHING`
159
+ : `INSERT OR IGNORE INTO analytics_rollup_backfill_state
160
+ (id, status, updated_at)
161
+ VALUES (?, 'pending', ${nowSql()})`;
162
+ }
163
+
164
+ function createBackfillLeaseToken(): string {
165
+ if (typeof globalThis.crypto?.randomUUID === "function") {
166
+ return globalThis.crypto.randomUUID();
167
+ }
168
+ return `analytics-rollup-${Date.now()}-${Math.random().toString(36).slice(2)}`;
169
+ }
170
+
171
+ function claimStateSql(): string {
172
+ return `UPDATE analytics_rollup_backfill_state
173
+ SET status = 'running', lease_token = ?,
174
+ lease_expires_at = datetime('now', '+${BACKFILL_LEASE_MINUTES} minutes'),
175
+ updated_at = datetime('now')
176
+ WHERE id = ?
177
+ AND (
178
+ status = 'pending'
179
+ OR (
180
+ status = 'running'
181
+ AND (
182
+ lease_expires_at IS NULL
183
+ OR lease_expires_at <= datetime('now')
184
+ )
185
+ )
186
+ )`;
187
+ }
188
+
189
+ function releaseStateLeaseSql(): string {
190
+ return `UPDATE analytics_rollup_backfill_state
191
+ SET status = 'pending', lease_token = NULL,
192
+ lease_expires_at = NULL, updated_at = datetime('now')
193
+ WHERE id = ? AND lease_token = ?`;
194
+ }
195
+
196
+ async function stateStatus(db: {
197
+ execute: (query: { sql: string; args: unknown[] }) => Promise<{
198
+ rows: any[];
199
+ rowsAffected: number;
200
+ }>;
201
+ }): Promise<string | null> {
202
+ const result = await db.execute({
203
+ sql: "SELECT status FROM analytics_rollup_backfill_state WHERE id = ?",
204
+ args: [BACKFILL_STATE_ID],
205
+ });
206
+ const status = result.rows[0]?.status;
207
+ return typeof status === "string" ? status : null;
208
+ }
209
+
210
+ async function completeState(tx: {
211
+ execute: (query: { sql: string; args: unknown[] }) => Promise<unknown>;
212
+ }): Promise<void> {
213
+ await tx.execute({
214
+ sql: `UPDATE analytics_rollup_backfill_state
215
+ SET status = 'completed', completed_at = ${nowSql()}, updated_at = ${nowSql()}
216
+ WHERE id = ?`,
217
+ args: [BACKFILL_STATE_ID],
218
+ });
219
+ }
220
+
221
+ async function runBackfillStatements(tx: {
222
+ execute: (query: string) => Promise<unknown>;
223
+ }): Promise<void> {
224
+ const statements = isPostgres()
225
+ ? POSTGRES_BACKFILL_STATEMENTS
226
+ : SQLITE_BACKFILL_STATEMENTS;
227
+ for (const statement of statements) await tx.execute(statement);
228
+ }
229
+
230
+ export async function isHistoricalAnalyticsRollupBackfillComplete(): Promise<boolean> {
231
+ return (await stateStatus(getDbExec())) === "completed";
232
+ }
233
+
234
+ async function runTransactionalBackfill(
235
+ db: ReturnType<typeof getDbExec>,
236
+ ): Promise<BackfillStatus> {
237
+ if (!db.transaction) {
238
+ throw new Error(
239
+ "Analytics rollup backfill requires a database transaction",
240
+ );
241
+ }
242
+
243
+ return db.transaction(async (tx) => {
244
+ if (isPostgres()) {
245
+ const lockResult = await tx.execute({
246
+ sql: "SELECT pg_try_advisory_xact_lock(hashtextextended(?, 0::bigint)) AS acquired",
247
+ args: [FIRST_PARTY_ANALYTICS_ROLLUP_LOCK_KEY],
248
+ });
249
+ const acquired = lockResult.rows[0]?.acquired;
250
+ if (acquired !== true && acquired !== "t") return "skipped-lock";
251
+ }
252
+
253
+ await tx.execute({ sql: ensureStateSql(), args: [BACKFILL_STATE_ID] });
254
+ if ((await stateStatus(tx)) === "completed") return "already-complete";
255
+
256
+ await runBackfillStatements(tx);
257
+ await completeState(tx);
258
+ return "completed";
259
+ });
260
+ }
261
+
262
+ async function runD1Backfill(
263
+ db: ReturnType<typeof getDbExec>,
264
+ ): Promise<BackfillStatus> {
265
+ if (!db.atomicBatch) {
266
+ throw new Error("D1 Analytics rollup backfill requires an atomic batch");
267
+ }
268
+
269
+ await db.execute({ sql: ensureStateSql(), args: [BACKFILL_STATE_ID] });
270
+ const leaseToken = createBackfillLeaseToken();
271
+ const claim = await db.execute({
272
+ sql: claimStateSql(),
273
+ args: [leaseToken, BACKFILL_STATE_ID],
274
+ });
275
+ if (claim.rowsAffected !== 1) {
276
+ return (await stateStatus(db)) === "completed"
277
+ ? "already-complete"
278
+ : "skipped-lock";
279
+ }
280
+
281
+ try {
282
+ const results = await db.atomicBatch([
283
+ ...SQLITE_BACKFILL_STATEMENTS,
284
+ {
285
+ sql: `UPDATE analytics_rollup_backfill_state
286
+ SET status = 'completed', completed_at = ${nowSql()},
287
+ lease_token = NULL, lease_expires_at = NULL,
288
+ updated_at = ${nowSql()}
289
+ WHERE id = ? AND status = 'running' AND lease_token = ?`,
290
+ args: [BACKFILL_STATE_ID, leaseToken],
291
+ },
292
+ ]);
293
+ if (results[results.length - 1]?.rowsAffected !== 1) {
294
+ throw new Error(
295
+ "Analytics rollup backfill lost its D1 lease before completion",
296
+ );
297
+ }
298
+ return "completed";
299
+ } catch (error) {
300
+ try {
301
+ await db.execute({
302
+ sql: releaseStateLeaseSql(),
303
+ args: [BACKFILL_STATE_ID, leaseToken],
304
+ });
305
+ } catch (releaseError) {
306
+ console.warn(
307
+ "[analytics-rollup-backfill] Failed to release D1 lease after backfill error:",
308
+ releaseError instanceof Error ? releaseError.message : releaseError,
309
+ );
310
+ }
311
+ throw error;
312
+ }
313
+ }
314
+
315
+ /**
316
+ * Rebuild historical compact rollups outside the server boot path. The state
317
+ * row is completed in the same transaction as both aggregates, so a timeout
318
+ * or failed write leaves the job pending and a later scheduled run retries it.
319
+ * D1 claims a short lease before its atomic batch so concurrent isolates do not
320
+ * each scan the full event history.
321
+ */
322
+ export async function runAnalyticsRollupBackfillOnce(): Promise<AnalyticsRollupBackfillResult> {
323
+ if (running) {
324
+ return { status: "already-running", remaining: 1 };
325
+ }
326
+ running = true;
327
+
328
+ try {
329
+ const db = getDbExec();
330
+ const status =
331
+ getDialect() === "d1"
332
+ ? await runD1Backfill(db)
333
+ : await runTransactionalBackfill(db);
334
+ return {
335
+ status,
336
+ remaining:
337
+ status === "completed" || status === "already-complete" ? 0 : 1,
338
+ };
339
+ } finally {
340
+ running = false;
341
+ }
342
+ }
@@ -18,13 +18,11 @@ export const INITIAL_TOOL_NAMES = [
18
18
  // while the agent lazily discovers the next mutating action.
19
19
  "get-sql-dashboard",
20
20
  "list-sql-dashboards",
21
- "list-dashboard-templates",
22
21
  "list-extensions",
23
22
  "get-extension",
24
23
  "update-dashboard",
25
24
  "mutate-dashboard",
26
25
  "compose-dashboard",
27
- "install-dashboard-template",
28
26
  "create-extension",
29
27
  "update-extension",
30
28
  "extension-data-set",
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Shared with the historical backfill so live ingest and migration work hold
3
+ * the same transaction-scoped Postgres lock while they update rollups.
4
+ */
5
+ export const FIRST_PARTY_ANALYTICS_ROLLUP_LOCK_KEY =
6
+ "agent-native:analytics-rollup-backfill";
7
+ export const FIRST_PARTY_ANALYTICS_ROLLUP_LOCK_SQL =
8
+ "SELECT pg_advisory_xact_lock(hashtextextended(?, 0::bigint))";
@@ -1,6 +1,10 @@
1
1
  import type { SqlDashboardConfig } from "../../app/pages/adhoc/sql-dashboard/types";
2
2
  import { loadDashboardSeed } from "./dashboard-seeds";
3
- import { listDashboards, type DashboardRecord } from "./dashboards-store";
3
+ import {
4
+ listDashboardSummaries,
5
+ type DashboardRecord,
6
+ type DashboardSummaryRecord,
7
+ } from "./dashboards-store";
4
8
  import {
5
9
  FIRST_PARTY_DASHBOARD_ID,
6
10
  repairFirstPartyObservedRetentionPanels,
@@ -1021,33 +1025,13 @@ export function cloneDashboardConfig(
1021
1025
  return JSON.parse(JSON.stringify(entry.buildConfig())) as SqlDashboardConfig;
1022
1026
  }
1023
1027
 
1024
- function templateIdFromConfig(config: Record<string, unknown>): string | null {
1025
- const catalog = config.catalog;
1026
- if (!catalog || typeof catalog !== "object" || Array.isArray(catalog)) {
1027
- return null;
1028
- }
1029
- const templateId = (catalog as Record<string, unknown>).templateId;
1030
- return typeof templateId === "string" && templateId ? templateId : null;
1031
- }
1032
-
1033
- function demoIdFromConfig(config: Record<string, unknown>): string | null {
1034
- const demo = config.demo;
1035
- if (!demo || typeof demo !== "object" || Array.isArray(demo)) {
1036
- return null;
1037
- }
1038
- const demoId = (demo as Record<string, unknown>).id;
1039
- return typeof demoId === "string" && demoId ? demoId : null;
1040
- }
1041
-
1042
1028
  function installedDashboardForTemplate(
1043
- row: DashboardRecord,
1029
+ row: DashboardSummaryRecord,
1044
1030
  entry: DashboardCatalogEntry,
1045
1031
  ): boolean {
1046
- const templateId = templateIdFromConfig(row.config);
1047
- const demoId = demoIdFromConfig(row.config);
1048
1032
  return (
1049
- templateId === entry.id ||
1050
- demoId === entry.id ||
1033
+ row.catalogTemplateId === entry.id ||
1034
+ row.demoId === entry.id ||
1051
1035
  row.id === entry.defaultDashboardId
1052
1036
  );
1053
1037
  }
@@ -1055,10 +1039,11 @@ function installedDashboardForTemplate(
1055
1039
  export async function listDashboardCatalog(
1056
1040
  ctx: AccessCtx,
1057
1041
  ): Promise<DashboardCatalogTemplate[]> {
1058
- const dashboards = await listDashboards(ctx, {
1042
+ const dashboards = await listDashboardSummaries(ctx, {
1059
1043
  kind: "sql",
1060
1044
  archived: "all",
1061
1045
  hidden: "all",
1046
+ includeCatalogMetadata: true,
1062
1047
  });
1063
1048
 
1064
1049
  return dashboardCatalogEntries
@@ -1068,10 +1053,7 @@ export async function listDashboardCatalog(
1068
1053
  .filter((row) => installedDashboardForTemplate(row, entry))
1069
1054
  .map((row) => ({
1070
1055
  id: row.id,
1071
- name:
1072
- typeof row.config.name === "string" && row.config.name.trim()
1073
- ? row.config.name
1074
- : row.title,
1056
+ name: row.configName?.trim() || row.name,
1075
1057
  visibility: row.visibility,
1076
1058
  updatedAt: row.updatedAt,
1077
1059
  archivedAt: row.archivedAt,
@@ -62,6 +62,9 @@ export interface DashboardSummaryRecord {
62
62
  id: string;
63
63
  kind: DashboardKind;
64
64
  name: string;
65
+ configName: string | null;
66
+ catalogTemplateId: string | null;
67
+ demoId: string | null;
65
68
  parentId: string | null;
66
69
  ownerEmail: string;
67
70
  orgId: string | null;
@@ -287,6 +290,24 @@ function configFromSettings(data: Record<string, unknown>): {
287
290
  return { title, config: data };
288
291
  }
289
292
 
293
+ function stringProperty(value: unknown, key: string): string | null {
294
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
295
+ const property = (value as Record<string, unknown>)[key];
296
+ return typeof property === "string" ? property : null;
297
+ }
298
+
299
+ function catalogMetadataFromConfig(config: Record<string, unknown>): {
300
+ configName: string | null;
301
+ catalogTemplateId: string | null;
302
+ demoId: string | null;
303
+ } {
304
+ return {
305
+ configName: typeof config.name === "string" ? config.name : null,
306
+ catalogTemplateId: stringProperty(config.catalog, "templateId"),
307
+ demoId: stringProperty(config.demo, "id"),
308
+ };
309
+ }
310
+
290
311
  async function migrateDashboardFromSettings(
291
312
  id: string,
292
313
  kind: DashboardKind,
@@ -510,11 +531,13 @@ export async function listDashboardSummaries(
510
531
  kind?: DashboardKind;
511
532
  archived?: DashboardArchiveFilter;
512
533
  hidden?: DashboardHiddenFilter;
534
+ includeCatalogMetadata?: boolean;
513
535
  },
514
536
  ): Promise<DashboardSummaryRecord[]> {
515
537
  const db = getDb() as any;
516
538
  const archived = filter?.archived ?? "active";
517
539
  const hidden = filter?.hidden ?? "visible";
540
+ const includeCatalogMetadata = filter?.includeCatalogMetadata === true;
518
541
  const conditions: any[] = [
519
542
  accessFilter(schema.dashboards, schema.dashboardShares, {
520
543
  userEmail: ctx.email,
@@ -535,11 +558,31 @@ export async function listDashboardSummaries(
535
558
  : sql<
536
559
  string | null
537
560
  >`json_extract(${schema.dashboards.config}, '$.parentId')`;
561
+ const configName = isPostgres()
562
+ ? sql<string | null>`(${schema.dashboards.config}::jsonb ->> 'name')`
563
+ : sql<string | null>`json_extract(${schema.dashboards.config}, '$.name')`;
564
+ const catalogTemplateId = isPostgres()
565
+ ? sql<
566
+ string | null
567
+ >`(${schema.dashboards.config}::jsonb -> 'catalog' ->> 'templateId')`
568
+ : sql<
569
+ string | null
570
+ >`json_extract(${schema.dashboards.config}, '$.catalog.templateId')`;
571
+ const demoId = isPostgres()
572
+ ? sql<
573
+ string | null
574
+ >`(${schema.dashboards.config}::jsonb -> 'demo' ->> 'id')`
575
+ : sql<
576
+ string | null
577
+ >`json_extract(${schema.dashboards.config}, '$.demo.id')`;
538
578
  const rows = await db
539
579
  .select({
540
580
  id: schema.dashboards.id,
541
581
  kind: schema.dashboards.kind,
542
582
  name: schema.dashboards.title,
583
+ ...(includeCatalogMetadata
584
+ ? { configName, catalogTemplateId, demoId }
585
+ : {}),
543
586
  parentId,
544
587
  ownerEmail: schema.dashboards.ownerEmail,
545
588
  orgId: schema.dashboards.orgId,
@@ -554,6 +597,10 @@ export async function listDashboardSummaries(
554
597
  .where(where);
555
598
  const out: DashboardSummaryRecord[] = rows.map((row: any) => ({
556
599
  ...row,
600
+ configName: typeof row.configName === "string" ? row.configName : null,
601
+ catalogTemplateId:
602
+ typeof row.catalogTemplateId === "string" ? row.catalogTemplateId : null,
603
+ demoId: typeof row.demoId === "string" ? row.demoId : null,
557
604
  parentId: typeof row.parentId === "string" ? row.parentId : null,
558
605
  orgId: row.orgId ?? null,
559
606
  archivedAt: row.archivedAt ?? null,
@@ -590,12 +637,20 @@ export async function listDashboardSummaries(
590
637
  seen.add(id);
591
638
  const config = value as Record<string, unknown>;
592
639
  const { title } = configFromSettings(config);
640
+ const catalogMetadata = includeCatalogMetadata
641
+ ? catalogMetadataFromConfig(config)
642
+ : {
643
+ configName: null,
644
+ catalogTemplateId: null,
645
+ demoId: null,
646
+ };
593
647
  const createdAt =
594
648
  typeof config.createdAt === "string" ? config.createdAt : nowIso();
595
649
  out.push({
596
650
  id,
597
651
  kind,
598
652
  name: title,
653
+ ...catalogMetadata,
599
654
  parentId: typeof config.parentId === "string" ? config.parentId : null,
600
655
  ownerEmail: ctx.email,
601
656
  orgId,