@agent-native/core 0.120.1 → 0.120.2

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 (170) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +9 -0
  3. package/corpus/core/docs/content/extensions.mdx +23 -4
  4. package/corpus/core/package.json +1 -1
  5. package/corpus/core/src/agent/production-agent.ts +4 -0
  6. package/corpus/core/src/agent/run-manager.ts +36 -10
  7. package/corpus/core/src/agent/types.ts +2 -0
  8. package/corpus/core/src/client/extensions/ExtensionsListPage.tsx +16 -3
  9. package/corpus/core/src/client/settings/ExtensionsSettingsContent.tsx +24 -0
  10. package/corpus/core/src/client/settings/SettingsPanel.tsx +10 -0
  11. package/corpus/core/src/deploy/build.ts +3 -0
  12. package/corpus/core/src/server/analytics.ts +154 -25
  13. package/corpus/core/src/templates/chat/app/components/layout/Sidebar.tsx +35 -6
  14. package/corpus/core/src/templates/chat/app/routes/extensions._index.tsx +2 -2
  15. package/corpus/core/src/templates/chat/netlify.toml +1 -0
  16. package/corpus/core/src/templates/workspace-core/.agents/skills/extensions/SKILL.md +16 -0
  17. package/corpus/core/src/vite/client.ts +6 -0
  18. package/corpus/templates/analytics/.agents/skills/adhoc-analysis/SKILL.md +27 -43
  19. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +19 -8
  20. package/corpus/templates/analytics/.agents/skills/data-querying/SKILL.md +2 -2
  21. package/corpus/templates/analytics/AGENTS.md +20 -8
  22. package/corpus/templates/analytics/actions/get-explorer-dashboard.ts +3 -0
  23. package/corpus/templates/analytics/actions/list-dashboard-usage-stats.ts +140 -46
  24. package/corpus/templates/analytics/actions/migrate-analytics-artifacts.ts +48 -0
  25. package/corpus/templates/analytics/actions/save-analysis.ts +2 -3
  26. package/corpus/templates/analytics/actions/view-screen.ts +1 -1
  27. package/corpus/templates/analytics/app/components/dashboard/DashboardMetadata.tsx +77 -0
  28. package/corpus/templates/analytics/app/components/dashboard/DashboardPanelSkeleton.tsx +7 -1
  29. package/corpus/templates/analytics/app/components/layout/NewDashboardDialog.tsx +1 -0
  30. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +103 -502
  31. package/corpus/templates/analytics/app/global.css +30 -0
  32. package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +2 -2
  33. package/corpus/templates/analytics/app/i18n-data.ts +80 -0
  34. package/corpus/templates/analytics/app/pages/Agents.tsx +49 -2
  35. package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/index.tsx +54 -11
  36. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +3 -53
  37. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +23 -25
  38. package/corpus/templates/analytics/app/pages/analyses/AnalysisDetail.tsx +2 -2
  39. package/corpus/templates/analytics/app/routes/analyses._index.tsx +3 -2
  40. package/corpus/templates/analytics/app/routes/extensions._index.tsx +2 -2
  41. package/corpus/templates/analytics/changelog/2026-07-23-analytics-now-groups-saved-analyses-and-embedded-extensions-.md +6 -0
  42. package/corpus/templates/analytics/changelog/2026-07-23-dashboard-menus-show-who-created-and-last-updated-each-dashb.md +6 -0
  43. package/corpus/templates/analytics/changelog/2026-07-23-dashboard-usage-now-loads-reliably-for-organizations-with-ma.md +6 -0
  44. package/corpus/templates/analytics/changelog/2026-07-23-long-dashboard-building-requests-now-continue-without-stoppi.md +6 -0
  45. package/corpus/templates/analytics/netlify.toml +4 -0
  46. package/corpus/templates/analytics/server/db/schema.ts +4 -0
  47. package/corpus/templates/analytics/server/lib/agent-chat-plan-mode.ts +0 -1
  48. package/corpus/templates/analytics/server/lib/agent-readable-resource-context.ts +1 -0
  49. package/corpus/templates/analytics/server/lib/migrate-analytics-artifacts.ts +1062 -0
  50. package/corpus/templates/analytics/server/plugins/agent-chat.ts +6 -52
  51. package/corpus/templates/analytics/server/plugins/core-routes.ts +1 -1
  52. package/corpus/templates/analytics/server/plugins/db.ts +10 -0
  53. package/corpus/templates/assets/actions/generate-image-batch.ts +36 -1
  54. package/corpus/templates/assets/actions/generate-image.ts +28 -1
  55. package/corpus/templates/assets/app/components/layout/Sidebar.tsx +46 -8
  56. package/corpus/templates/assets/app/routes/extensions._index.tsx +2 -2
  57. package/corpus/templates/assets/changelog/2026-07-23-assets-image-generation-now-recovers-after-a-completed-tool-.md +6 -0
  58. package/corpus/templates/assets/netlify.toml +1 -0
  59. package/corpus/templates/brain/app/components/layout/Sidebar.tsx +38 -8
  60. package/corpus/templates/brain/app/routes/extensions._index.tsx +2 -2
  61. package/corpus/templates/brain/netlify.toml +1 -0
  62. package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +43 -5
  63. package/corpus/templates/calendar/app/routes/_app.extensions._index.tsx +2 -16
  64. package/corpus/templates/calendar/netlify.toml +1 -0
  65. package/corpus/templates/chat/app/components/layout/Sidebar.tsx +35 -6
  66. package/corpus/templates/chat/app/routes/extensions._index.tsx +2 -2
  67. package/corpus/templates/chat/netlify.toml +1 -0
  68. package/corpus/templates/clips/app/components/library/library-layout.tsx +52 -6
  69. package/corpus/templates/clips/app/routes/_app.extensions._index.tsx +2 -16
  70. package/corpus/templates/clips/changelog/2026-07-23-agent-settings-are-clearly-labeled-manage-agent-in-the-sideb.md +6 -0
  71. package/corpus/templates/clips/changelog/2026-07-23-meeting-recording-meters-and-dismissed-prompts.md +6 -0
  72. package/corpus/templates/clips/desktop/src/hooks/useMicMeter.ts +1 -1
  73. package/corpus/templates/clips/desktop/src/lib/meeting-notification-dismissal.ts +2 -0
  74. package/corpus/templates/clips/desktop/src/lib/voice-dictation.ts +1 -3
  75. package/corpus/templates/clips/desktop/src/overlays/live-audio-bars.tsx +86 -0
  76. package/corpus/templates/clips/desktop/src/overlays/meeting-notification.tsx +24 -0
  77. package/corpus/templates/clips/desktop/src/overlays/recording-pill.tsx +5 -69
  78. package/corpus/templates/clips/desktop/src/styles.css +35 -3
  79. package/corpus/templates/clips/desktop/src-tauri/src/adhoc_meetings_watcher.rs +13 -44
  80. package/corpus/templates/clips/desktop/src-tauri/src/notifications.rs +119 -17
  81. package/corpus/templates/clips/netlify.toml +1 -0
  82. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +3 -7
  83. package/corpus/templates/content/app/routes/_app.extensions._index.tsx +2 -2
  84. package/corpus/templates/content/changelog/2026-07-23-agent-settings-are-clearly-labeled-manage-agent-in-the-sideb.md +6 -0
  85. package/corpus/templates/content/netlify.toml +1 -0
  86. package/corpus/templates/design/app/components/layout/Sidebar.tsx +55 -17
  87. package/corpus/templates/design/app/routes/extensions._index.tsx +2 -2
  88. package/corpus/templates/design/changelog/2026-07-23-agent-settings-are-clearly-labeled-manage-agent-in-the-sideb.md +6 -0
  89. package/corpus/templates/design/netlify.toml +1 -0
  90. package/corpus/templates/dispatch/app/dispatch-extensions.tsx +1 -1
  91. package/corpus/templates/dispatch/changelog/2026-07-23-agent-settings-are-clearly-labeled-manage-agent-in-the-sideb.md +6 -0
  92. package/corpus/templates/dispatch/netlify.toml +1 -0
  93. package/corpus/templates/forms/app/components/layout/Sidebar.tsx +41 -45
  94. package/corpus/templates/forms/app/routes/_app.extensions._index.tsx +2 -2
  95. package/corpus/templates/forms/changelog/2026-07-23-agent-settings-are-clearly-labeled-manage-agent-in-the-sideb.md +6 -0
  96. package/corpus/templates/forms/netlify.toml +1 -0
  97. package/corpus/templates/macros/app/components/layout/AppLayout.tsx +36 -5
  98. package/corpus/templates/macros/app/routes/extensions._index.tsx +2 -2
  99. package/corpus/templates/macros/netlify.toml +1 -0
  100. package/corpus/templates/mail/app/components/layout/AppLayout.tsx +0 -9
  101. package/corpus/templates/mail/app/routes/extensions._index.tsx +2 -2
  102. package/corpus/templates/mail/netlify.toml +1 -0
  103. package/corpus/templates/plan/app/components/layout/Sidebar.tsx +38 -5
  104. package/corpus/templates/plan/app/routes/extensions._index.tsx +2 -2
  105. package/corpus/templates/plan/netlify.toml +1 -0
  106. package/corpus/templates/slides/app/components/layout/Sidebar.tsx +47 -4
  107. package/corpus/templates/slides/app/routes/extensions._index.tsx +2 -2
  108. package/corpus/templates/slides/netlify.toml +1 -0
  109. package/corpus/templates/tasks/app/components/layout/Sidebar.tsx +37 -6
  110. package/corpus/templates/tasks/app/routes/extensions._index.tsx +2 -2
  111. package/corpus/templates/tasks/app/routes/settings.tsx +31 -0
  112. package/corpus/templates/tasks/netlify.toml +1 -0
  113. package/dist/agent/production-agent.d.ts.map +1 -1
  114. package/dist/agent/production-agent.js +4 -0
  115. package/dist/agent/production-agent.js.map +1 -1
  116. package/dist/agent/run-manager.d.ts +9 -0
  117. package/dist/agent/run-manager.d.ts.map +1 -1
  118. package/dist/agent/run-manager.js +25 -10
  119. package/dist/agent/run-manager.js.map +1 -1
  120. package/dist/agent/types.d.ts +2 -2
  121. package/dist/agent/types.d.ts.map +1 -1
  122. package/dist/agent/types.js.map +1 -1
  123. package/dist/client/extensions/ExtensionsListPage.d.ts +5 -1
  124. package/dist/client/extensions/ExtensionsListPage.d.ts.map +1 -1
  125. package/dist/client/extensions/ExtensionsListPage.js +5 -3
  126. package/dist/client/extensions/ExtensionsListPage.js.map +1 -1
  127. package/dist/client/settings/ExtensionsSettingsContent.d.ts +2 -0
  128. package/dist/client/settings/ExtensionsSettingsContent.d.ts.map +1 -0
  129. package/dist/client/settings/ExtensionsSettingsContent.js +10 -0
  130. package/dist/client/settings/ExtensionsSettingsContent.js.map +1 -0
  131. package/dist/client/settings/SettingsPanel.d.ts.map +1 -1
  132. package/dist/client/settings/SettingsPanel.js +10 -1
  133. package/dist/client/settings/SettingsPanel.js.map +1 -1
  134. package/dist/collab/awareness.d.ts +2 -2
  135. package/dist/collab/awareness.d.ts.map +1 -1
  136. package/dist/deploy/build.js +1 -0
  137. package/dist/deploy/build.js.map +1 -1
  138. package/dist/observability/routes.d.ts +3 -3
  139. package/dist/progress/routes.d.ts +1 -1
  140. package/dist/provider-api/actions/custom-provider-registration.d.ts +7 -7
  141. package/dist/provider-api/actions/provider-api.d.ts +11 -11
  142. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  143. package/dist/server/analytics.d.ts +9 -4
  144. package/dist/server/analytics.d.ts.map +1 -1
  145. package/dist/server/analytics.js +111 -22
  146. package/dist/server/analytics.js.map +1 -1
  147. package/dist/templates/chat/app/components/layout/Sidebar.tsx +35 -6
  148. package/dist/templates/chat/app/routes/extensions._index.tsx +2 -2
  149. package/dist/templates/chat/netlify.toml +1 -0
  150. package/dist/templates/workspace-core/.agents/skills/extensions/SKILL.md +16 -0
  151. package/dist/vite/client.d.ts.map +1 -1
  152. package/dist/vite/client.js +2 -0
  153. package/dist/vite/client.js.map +1 -1
  154. package/docs/content/extensions.mdx +23 -4
  155. package/package.json +1 -1
  156. package/src/agent/production-agent.ts +4 -0
  157. package/src/agent/run-manager.ts +36 -10
  158. package/src/agent/types.ts +2 -0
  159. package/src/client/extensions/ExtensionsListPage.tsx +16 -3
  160. package/src/client/settings/ExtensionsSettingsContent.tsx +24 -0
  161. package/src/client/settings/SettingsPanel.tsx +10 -0
  162. package/src/deploy/build.ts +3 -0
  163. package/src/server/analytics.ts +154 -25
  164. package/src/templates/chat/app/components/layout/Sidebar.tsx +35 -6
  165. package/src/templates/chat/app/routes/extensions._index.tsx +2 -2
  166. package/src/templates/chat/netlify.toml +1 -0
  167. package/src/templates/workspace-core/.agents/skills/extensions/SKILL.md +16 -0
  168. package/src/vite/client.ts +6 -0
  169. package/corpus/templates/analytics/app/components/layout/NewAnalysisDialog.tsx +0 -165
  170. package/corpus/templates/analytics/app/pages/analyses/AnalysesList.tsx +0 -175
@@ -0,0 +1,1062 @@
1
+ import { randomUUID } from "node:crypto";
2
+
3
+ import {
4
+ createSharesTable,
5
+ ownableColumns,
6
+ table,
7
+ text,
8
+ } from "@agent-native/core/db/schema";
9
+ import { recordChange } from "@agent-native/core/server";
10
+ import { listOrgSettings } from "@agent-native/core/settings";
11
+ import { eq, inArray } from "drizzle-orm";
12
+
13
+ import { getDb, schema } from "../db/index.js";
14
+
15
+ const migrationExtensions = table("tools", {
16
+ id: text("id").primaryKey(),
17
+ name: text("name").notNull(),
18
+ description: text("description").notNull(),
19
+ content: text("content").notNull(),
20
+ icon: text("icon"),
21
+ createdAt: text("created_at").notNull(),
22
+ updatedAt: text("updated_at").notNull(),
23
+ archivedAt: text("archived_at"),
24
+ hiddenAt: text("hidden_at"),
25
+ hiddenBy: text("hidden_by"),
26
+ ...ownableColumns(),
27
+ });
28
+
29
+ const migrationExtensionData = table("tool_data", {
30
+ id: text("id").primaryKey(),
31
+ extensionId: text("tool_id").notNull(),
32
+ orgId: text("org_id"),
33
+ });
34
+
35
+ const migrationExtensionShares = createSharesTable("tool_shares");
36
+ const migrationSettings = table("settings", {
37
+ key: text("key").primaryKey(),
38
+ value: text("value").notNull(),
39
+ });
40
+
41
+ type Visibility = "private" | "org" | "public";
42
+ type SourceType = "analysis" | "extension";
43
+
44
+ export interface AnalyticsArtifactMigrationContext {
45
+ userEmail: string;
46
+ orgId: string;
47
+ }
48
+
49
+ export interface AnalyticsArtifactMigrationSummary {
50
+ dryRun: boolean;
51
+ orgId: string;
52
+ legacyDashboards: number;
53
+ legacyAnalyses: number;
54
+ dashboardsMaterialized: number;
55
+ analysesMaterialized: number;
56
+ dashboardsCreated: number;
57
+ analysisDashboardsCreated: number;
58
+ extensionDashboardsCreated: number;
59
+ duplicateDashboardsArchived: number;
60
+ duplicateAnalysesHidden: number;
61
+ duplicateExtensionsArchived: number;
62
+ analysesHidden: number;
63
+ extensionsHidden: number;
64
+ legacySettingsDeleted: number;
65
+ dashboardReferencesRewritten: number;
66
+ skipped: Array<{ type: string; id: string; reason: string }>;
67
+ }
68
+
69
+ interface LegacyDashboard {
70
+ id: string;
71
+ kind: "explorer" | "sql";
72
+ data: Record<string, unknown>;
73
+ }
74
+
75
+ interface LegacyAnalysis {
76
+ id: string;
77
+ data: Record<string, unknown>;
78
+ }
79
+
80
+ interface DashboardSource {
81
+ id: string;
82
+ kind: "explorer" | "sql";
83
+ title: string;
84
+ config: Record<string, unknown>;
85
+ ownerEmail: string;
86
+ orgId: string;
87
+ visibility: Visibility;
88
+ createdAt: string;
89
+ updatedAt: string;
90
+ archivedAt: string | null;
91
+ hiddenAt: string | null;
92
+ }
93
+
94
+ interface AnalysisSource {
95
+ id: string;
96
+ name: string;
97
+ description: string;
98
+ question: string;
99
+ instructions: string;
100
+ dataSources: string[];
101
+ resultMarkdown: string;
102
+ resultData: Record<string, unknown> | null;
103
+ author: string | null;
104
+ ownerEmail: string;
105
+ orgId: string;
106
+ visibility: Visibility;
107
+ createdAt: string;
108
+ updatedAt: string;
109
+ hiddenAt: string | null;
110
+ }
111
+
112
+ interface ExtensionSource {
113
+ id: string;
114
+ name: string;
115
+ description: string;
116
+ content: string;
117
+ icon: string | null;
118
+ createdAt: string;
119
+ updatedAt: string;
120
+ archivedAt: string | null;
121
+ hiddenAt: string | null;
122
+ ownerEmail: string;
123
+ orgId: string;
124
+ visibility: Visibility;
125
+ }
126
+
127
+ interface MigrationState {
128
+ dashboards: DashboardSource[];
129
+ analyses: AnalysisSource[];
130
+ extensions: ExtensionSource[];
131
+ legacyDashboardKeys: string[];
132
+ legacyAnalysisKeys: string[];
133
+ extensionDataIds: Set<string>;
134
+ }
135
+
136
+ function nowIso(): string {
137
+ return new Date().toISOString();
138
+ }
139
+
140
+ function parseJson(value: unknown): Record<string, unknown> {
141
+ if (value && typeof value === "object" && !Array.isArray(value)) {
142
+ return value as Record<string, unknown>;
143
+ }
144
+ if (typeof value !== "string") return {};
145
+ try {
146
+ const parsed = JSON.parse(value);
147
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed)
148
+ ? (parsed as Record<string, unknown>)
149
+ : {};
150
+ } catch {
151
+ return {};
152
+ }
153
+ }
154
+
155
+ function parseArray<T>(value: unknown): T[] {
156
+ if (Array.isArray(value)) return value as T[];
157
+ if (typeof value !== "string") return [];
158
+ try {
159
+ const parsed = JSON.parse(value);
160
+ return Array.isArray(parsed) ? (parsed as T[]) : [];
161
+ } catch {
162
+ return [];
163
+ }
164
+ }
165
+
166
+ function stableValue(value: unknown): unknown {
167
+ if (Array.isArray(value)) return value.map(stableValue);
168
+ if (value && typeof value === "object") {
169
+ return Object.fromEntries(
170
+ Object.entries(value as Record<string, unknown>)
171
+ .sort(([a], [b]) => a.localeCompare(b))
172
+ .map(([key, nested]) => [key, stableValue(nested)]),
173
+ );
174
+ }
175
+ return value;
176
+ }
177
+
178
+ function stableStringify(value: unknown): string {
179
+ return JSON.stringify(stableValue(value));
180
+ }
181
+
182
+ function scopedFingerprint(
183
+ visibility: Visibility,
184
+ ownerEmail: string,
185
+ value: unknown,
186
+ ): string {
187
+ return stableStringify({
188
+ scope: visibility === "org" ? "org" : ownerEmail.toLowerCase(),
189
+ visibility,
190
+ value,
191
+ });
192
+ }
193
+
194
+ function dashboardTitle(data: Record<string, unknown>): string {
195
+ const title = data.title ?? data.name;
196
+ return typeof title === "string" && title.trim() ? title.trim() : "Untitled";
197
+ }
198
+
199
+ function timestamp(data: Record<string, unknown>, fallback: string): string {
200
+ return typeof data.updatedAt === "string" && data.updatedAt
201
+ ? data.updatedAt
202
+ : typeof data.createdAt === "string" && data.createdAt
203
+ ? data.createdAt
204
+ : fallback;
205
+ }
206
+
207
+ function legacyDashboardRows(
208
+ settings: Record<string, Record<string, unknown>>,
209
+ ): { rows: LegacyDashboard[]; keys: string[] } {
210
+ const rows: LegacyDashboard[] = [];
211
+ const keys: string[] = [];
212
+ for (const [key, data] of Object.entries(settings)) {
213
+ if (key.startsWith("sql-dashboard-")) {
214
+ rows.push({
215
+ id: key.slice("sql-dashboard-".length),
216
+ kind: "sql",
217
+ data,
218
+ });
219
+ keys.push(key);
220
+ } else if (key.startsWith("dashboard-")) {
221
+ rows.push({
222
+ id: key.slice("dashboard-".length),
223
+ kind: "explorer",
224
+ data,
225
+ });
226
+ keys.push(key);
227
+ }
228
+ }
229
+ return { rows, keys };
230
+ }
231
+
232
+ function legacyAnalysisRows(
233
+ settings: Record<string, Record<string, unknown>>,
234
+ ): { rows: LegacyAnalysis[]; keys: string[] } {
235
+ const rows: LegacyAnalysis[] = [];
236
+ const keys: string[] = [];
237
+ for (const [key, data] of Object.entries(settings)) {
238
+ if (!key.startsWith("adhoc-analysis-")) continue;
239
+ rows.push({ id: key.slice("adhoc-analysis-".length), data });
240
+ keys.push(key);
241
+ }
242
+ return { rows, keys };
243
+ }
244
+
245
+ async function readMigrationState(
246
+ ctx: AnalyticsArtifactMigrationContext,
247
+ ): Promise<MigrationState> {
248
+ const db = getDb() as any;
249
+ const orgSettings = await listOrgSettings(ctx.orgId);
250
+ const legacyDashboards = legacyDashboardRows(orgSettings);
251
+ const legacyAnalyses = legacyAnalysisRows(orgSettings);
252
+
253
+ const [dashboardRows, analysisRows, extensionRows] = await Promise.all([
254
+ db
255
+ .select()
256
+ .from(schema.dashboards)
257
+ .where(eq(schema.dashboards.orgId, ctx.orgId)),
258
+ db
259
+ .select()
260
+ .from(schema.analyses)
261
+ .where(eq(schema.analyses.orgId, ctx.orgId)),
262
+ db
263
+ .select()
264
+ .from(migrationExtensions)
265
+ .where(eq(migrationExtensions.orgId, ctx.orgId)),
266
+ ]);
267
+ const extensionIds = extensionRows.map((row: { id: string }) => row.id);
268
+ const extensionDataRows =
269
+ extensionIds.length === 0
270
+ ? []
271
+ : await db
272
+ .select({ extensionId: migrationExtensionData.extensionId })
273
+ .from(migrationExtensionData)
274
+ .where(inArray(migrationExtensionData.extensionId, extensionIds));
275
+
276
+ const now = nowIso();
277
+ const materializedDashboardIds = new Set(
278
+ dashboardRows.map((row: { id: string }) => row.id),
279
+ );
280
+ const materializedAnalysisIds = new Set(
281
+ analysisRows.map((row: { id: string }) => row.id),
282
+ );
283
+
284
+ const dashboards: DashboardSource[] = dashboardRows.map((row: any) => ({
285
+ id: row.id,
286
+ kind: row.kind,
287
+ title: row.title,
288
+ config: parseJson(row.config),
289
+ ownerEmail: row.ownerEmail,
290
+ orgId: row.orgId,
291
+ visibility: row.visibility,
292
+ createdAt: row.createdAt,
293
+ updatedAt: row.updatedAt,
294
+ archivedAt: row.archivedAt ?? null,
295
+ hiddenAt: row.hiddenAt ?? null,
296
+ }));
297
+
298
+ for (const row of legacyDashboards.rows) {
299
+ if (materializedDashboardIds.has(row.id)) continue;
300
+ dashboards.push({
301
+ id: row.id,
302
+ kind: row.kind,
303
+ title: dashboardTitle(row.data),
304
+ config: row.data,
305
+ ownerEmail: ctx.userEmail,
306
+ orgId: ctx.orgId,
307
+ visibility: "org",
308
+ createdAt:
309
+ typeof row.data.createdAt === "string" ? row.data.createdAt : now,
310
+ updatedAt: timestamp(row.data, now),
311
+ archivedAt: null,
312
+ hiddenAt: null,
313
+ });
314
+ }
315
+
316
+ const analyses: AnalysisSource[] = analysisRows.map((row: any) => ({
317
+ id: row.id,
318
+ name: row.name,
319
+ description: row.description,
320
+ question: row.question,
321
+ instructions: row.instructions,
322
+ dataSources: parseArray<string>(row.dataSources),
323
+ resultMarkdown: row.resultMarkdown,
324
+ resultData: row.resultData ? parseJson(row.resultData) : null,
325
+ author: row.author ?? null,
326
+ ownerEmail: row.ownerEmail,
327
+ orgId: row.orgId,
328
+ visibility: row.visibility,
329
+ createdAt: row.createdAt,
330
+ updatedAt: row.updatedAt,
331
+ hiddenAt: row.hiddenAt ?? null,
332
+ }));
333
+
334
+ for (const row of legacyAnalyses.rows) {
335
+ if (materializedAnalysisIds.has(row.id)) continue;
336
+ analyses.push({
337
+ id: row.id,
338
+ name: typeof row.data.name === "string" ? row.data.name : "Untitled",
339
+ description:
340
+ typeof row.data.description === "string" ? row.data.description : "",
341
+ question: typeof row.data.question === "string" ? row.data.question : "",
342
+ instructions:
343
+ typeof row.data.instructions === "string" ? row.data.instructions : "",
344
+ dataSources: parseArray<string>(row.data.dataSources),
345
+ resultMarkdown:
346
+ typeof row.data.resultMarkdown === "string"
347
+ ? row.data.resultMarkdown
348
+ : "",
349
+ resultData: row.data.resultData ? parseJson(row.data.resultData) : null,
350
+ author:
351
+ typeof row.data.author === "string" ? row.data.author : ctx.userEmail,
352
+ ownerEmail: ctx.userEmail,
353
+ orgId: ctx.orgId,
354
+ visibility: "org",
355
+ createdAt:
356
+ typeof row.data.createdAt === "string" ? row.data.createdAt : now,
357
+ updatedAt: timestamp(row.data, now),
358
+ hiddenAt: null,
359
+ });
360
+ }
361
+
362
+ return {
363
+ dashboards,
364
+ analyses,
365
+ extensions: extensionRows.map((row: any) => ({
366
+ id: row.id,
367
+ name: row.name,
368
+ description: row.description,
369
+ content: row.content,
370
+ icon: row.icon ?? null,
371
+ createdAt: row.createdAt,
372
+ updatedAt: row.updatedAt,
373
+ archivedAt: row.archivedAt ?? null,
374
+ hiddenAt: row.hiddenAt ?? null,
375
+ ownerEmail: row.ownerEmail,
376
+ orgId: row.orgId,
377
+ visibility: row.visibility,
378
+ })),
379
+ legacyDashboardKeys: legacyDashboards.keys,
380
+ legacyAnalysisKeys: legacyAnalyses.keys,
381
+ extensionDataIds: new Set(
382
+ extensionDataRows.map((row: { extensionId: string }) => row.extensionId),
383
+ ),
384
+ };
385
+ }
386
+
387
+ function duplicateDashboardGroups(
388
+ rows: DashboardSource[],
389
+ ): DashboardSource[][] {
390
+ const groups = new Map<string, DashboardSource[]>();
391
+ for (const row of rows) {
392
+ if (row.archivedAt) continue;
393
+ const key = scopedFingerprint(row.visibility, row.ownerEmail, {
394
+ kind: row.kind,
395
+ title: row.title,
396
+ config: row.config,
397
+ });
398
+ const group = groups.get(key) ?? [];
399
+ group.push(row);
400
+ groups.set(key, group);
401
+ }
402
+ return Array.from(groups.values()).filter((group) => group.length > 1);
403
+ }
404
+
405
+ function duplicateAnalysisGroups(rows: AnalysisSource[]): AnalysisSource[][] {
406
+ const groups = new Map<string, AnalysisSource[]>();
407
+ for (const row of rows) {
408
+ const key = scopedFingerprint(row.visibility, row.ownerEmail, {
409
+ name: row.name,
410
+ description: row.description,
411
+ question: row.question,
412
+ instructions: row.instructions,
413
+ dataSources: row.dataSources,
414
+ resultMarkdown: row.resultMarkdown,
415
+ resultData: row.resultData,
416
+ });
417
+ const group = groups.get(key) ?? [];
418
+ group.push(row);
419
+ groups.set(key, group);
420
+ }
421
+ return Array.from(groups.values()).filter((group) => group.length > 1);
422
+ }
423
+
424
+ function duplicateExtensionGroups(
425
+ rows: ExtensionSource[],
426
+ ): ExtensionSource[][] {
427
+ const groups = new Map<string, ExtensionSource[]>();
428
+ for (const row of rows) {
429
+ if (row.archivedAt) continue;
430
+ const key = scopedFingerprint(row.visibility, row.ownerEmail, {
431
+ name: row.name,
432
+ description: row.description,
433
+ content: row.content,
434
+ icon: row.icon,
435
+ });
436
+ const group = groups.get(key) ?? [];
437
+ group.push(row);
438
+ groups.set(key, group);
439
+ }
440
+ return Array.from(groups.values()).filter((group) => group.length > 1);
441
+ }
442
+
443
+ function oldest<T extends { createdAt: string; id: string }>(rows: T[]): T {
444
+ return [...rows].sort(
445
+ (a, b) =>
446
+ a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id),
447
+ )[0];
448
+ }
449
+
450
+ function escapeHtml(value: string): string {
451
+ return value.replace(/[&<>"']/g, (character) => {
452
+ switch (character) {
453
+ case "&":
454
+ return "&amp;";
455
+ case "<":
456
+ return "&lt;";
457
+ case ">":
458
+ return "&gt;";
459
+ case '"':
460
+ return "&quot;";
461
+ default:
462
+ return "&#39;";
463
+ }
464
+ });
465
+ }
466
+
467
+ function analysisExtensionContent(analysis: AnalysisSource): string {
468
+ const sourceList = analysis.dataSources.length
469
+ ? `<p><strong>Data sources:</strong> ${escapeHtml(analysis.dataSources.join(", "))}</p>`
470
+ : "";
471
+ const question = analysis.question
472
+ ? `<p><strong>Question:</strong> ${escapeHtml(analysis.question)}</p>`
473
+ : "";
474
+ const description = analysis.description
475
+ ? `<p>${escapeHtml(analysis.description)}</p>`
476
+ : "";
477
+ const result = analysis.resultMarkdown || "No saved result.";
478
+ return `<article style="font-family:system-ui,sans-serif;color:inherit;line-height:1.5;padding:20px;max-width:900px;margin:auto"><h1 style="font-size:20px;margin:0 0 12px">${escapeHtml(analysis.name)}</h1>${description}${question}${sourceList}<pre style="white-space:pre-wrap;font:inherit;margin:20px 0 0">${escapeHtml(result)}</pre></article>`;
479
+ }
480
+
481
+ function migratedDashboardConfig(
482
+ name: string,
483
+ description: string,
484
+ extensionId: string,
485
+ sourceType: SourceType,
486
+ sourceId: string,
487
+ ): Record<string, unknown> {
488
+ return {
489
+ name,
490
+ description,
491
+ migration: { sourceType, sourceId },
492
+ panels: [
493
+ {
494
+ id: `${sourceType}-content`,
495
+ title: name,
496
+ sql: "",
497
+ source: "first-party",
498
+ chartType: "extension",
499
+ width: 1,
500
+ config: { extensionId },
501
+ },
502
+ ],
503
+ };
504
+ }
505
+
506
+ function sourceMigrationKey(config: Record<string, unknown>): string | null {
507
+ const migration = config.migration;
508
+ if (!migration || typeof migration !== "object") return null;
509
+ const source = migration as Record<string, unknown>;
510
+ if (
511
+ (source.sourceType === "analysis" || source.sourceType === "extension") &&
512
+ typeof source.sourceId === "string"
513
+ ) {
514
+ return `${source.sourceType}:${source.sourceId}`;
515
+ }
516
+ return null;
517
+ }
518
+
519
+ function collectExtensionIds(
520
+ value: unknown,
521
+ output = new Set<string>(),
522
+ ): Set<string> {
523
+ if (Array.isArray(value)) {
524
+ for (const item of value) collectExtensionIds(item, output);
525
+ return output;
526
+ }
527
+ if (!value || typeof value !== "object") return output;
528
+ for (const [key, nested] of Object.entries(
529
+ value as Record<string, unknown>,
530
+ )) {
531
+ if (key === "extensionId" && typeof nested === "string") {
532
+ output.add(nested);
533
+ }
534
+ collectExtensionIds(nested, output);
535
+ }
536
+ return output;
537
+ }
538
+
539
+ function remapExtensionIds(
540
+ value: unknown,
541
+ replacements: Map<string, string>,
542
+ ): { value: unknown; changed: boolean } {
543
+ if (Array.isArray(value)) {
544
+ let changed = false;
545
+ const next = value.map((item) => {
546
+ const result = remapExtensionIds(item, replacements);
547
+ changed ||= result.changed;
548
+ return result.value;
549
+ });
550
+ return { value: next, changed };
551
+ }
552
+ if (!value || typeof value !== "object") return { value, changed: false };
553
+ let changed = false;
554
+ const next: Record<string, unknown> = {};
555
+ for (const [key, nested] of Object.entries(
556
+ value as Record<string, unknown>,
557
+ )) {
558
+ if (key === "extensionId" && typeof nested === "string") {
559
+ const replacement = replacements.get(nested);
560
+ if (replacement) {
561
+ next[key] = replacement;
562
+ changed = true;
563
+ continue;
564
+ }
565
+ }
566
+ const result = remapExtensionIds(nested, replacements);
567
+ next[key] = result.value;
568
+ changed ||= result.changed;
569
+ }
570
+ return { value: next, changed };
571
+ }
572
+
573
+ async function copySharesBatch(
574
+ tx: any,
575
+ sourceTable: any,
576
+ targetTable: any,
577
+ pairs: Array<{ sourceId: string; targetId: string }>,
578
+ runId: string,
579
+ ): Promise<void> {
580
+ if (pairs.length === 0) return;
581
+ const sourceIds = [...new Set(pairs.map((pair) => pair.sourceId))];
582
+ const targetIds = [...new Set(pairs.map((pair) => pair.targetId))];
583
+ const targetBySource = new Map(
584
+ pairs.map((pair) => [pair.sourceId, pair.targetId]),
585
+ );
586
+ const [sourceRows, targetRows] = await Promise.all([
587
+ tx
588
+ .select()
589
+ .from(sourceTable)
590
+ .where(inArray(sourceTable.resourceId, sourceIds)),
591
+ tx
592
+ .select()
593
+ .from(targetTable)
594
+ .where(inArray(targetTable.resourceId, targetIds)),
595
+ ]);
596
+ const existing = new Set(
597
+ targetRows.map(
598
+ (row: any) =>
599
+ `${row.resourceId}:${row.principalType}:${row.principalId}:${row.role}`,
600
+ ),
601
+ );
602
+ const values = sourceRows
603
+ .filter((row: any) => {
604
+ const targetId = targetBySource.get(row.resourceId);
605
+ if (!targetId) return false;
606
+ const key = `${targetId}:${row.principalType}:${row.principalId}:${row.role}`;
607
+ if (existing.has(key)) return false;
608
+ existing.add(key);
609
+ return true;
610
+ })
611
+ .map((row: any) => ({
612
+ resourceId: targetBySource.get(row.resourceId),
613
+ id: `${runId}-${randomUUID()}`,
614
+ principalType: row.principalType,
615
+ principalId: row.principalId,
616
+ role: row.role,
617
+ createdBy: row.createdBy,
618
+ createdAt: row.createdAt,
619
+ }));
620
+ if (values.length > 0) await tx.insert(targetTable).values(values);
621
+ }
622
+
623
+ function summaryTemplate(
624
+ ctx: AnalyticsArtifactMigrationContext,
625
+ dryRun: boolean,
626
+ ): AnalyticsArtifactMigrationSummary {
627
+ return {
628
+ dryRun,
629
+ orgId: ctx.orgId,
630
+ legacyDashboards: 0,
631
+ legacyAnalyses: 0,
632
+ dashboardsMaterialized: 0,
633
+ analysesMaterialized: 0,
634
+ dashboardsCreated: 0,
635
+ analysisDashboardsCreated: 0,
636
+ extensionDashboardsCreated: 0,
637
+ duplicateDashboardsArchived: 0,
638
+ duplicateAnalysesHidden: 0,
639
+ duplicateExtensionsArchived: 0,
640
+ analysesHidden: 0,
641
+ extensionsHidden: 0,
642
+ legacySettingsDeleted: 0,
643
+ dashboardReferencesRewritten: 0,
644
+ skipped: [],
645
+ };
646
+ }
647
+
648
+ export async function migrateAnalyticsArtifacts(
649
+ ctx: AnalyticsArtifactMigrationContext,
650
+ options: { dryRun: boolean },
651
+ ): Promise<AnalyticsArtifactMigrationSummary> {
652
+ const state = await readMigrationState(ctx);
653
+ const summary = summaryTemplate(ctx, options.dryRun);
654
+ summary.legacyDashboards = state.legacyDashboardKeys.length;
655
+ summary.legacyAnalyses = state.legacyAnalysisKeys.length;
656
+ const legacyDashboardIds = new Set(
657
+ state.legacyDashboardKeys.map((key) =>
658
+ key.startsWith("sql-dashboard-")
659
+ ? key.slice("sql-dashboard-".length)
660
+ : key.slice("dashboard-".length),
661
+ ),
662
+ );
663
+ const legacyAnalysisIds = new Set(
664
+ state.legacyAnalysisKeys.map((key) => key.slice("adhoc-analysis-".length)),
665
+ );
666
+ summary.dashboardsMaterialized = state.dashboards.filter((row) =>
667
+ legacyDashboardIds.has(row.id),
668
+ ).length;
669
+ summary.analysesMaterialized = state.analyses.filter((row) =>
670
+ legacyAnalysisIds.has(row.id),
671
+ ).length;
672
+
673
+ const dashboardDuplicates = duplicateDashboardGroups(state.dashboards);
674
+ const analysisDuplicates = duplicateAnalysisGroups(state.analyses);
675
+ const extensionDuplicates = duplicateExtensionGroups(state.extensions);
676
+ const duplicateDashboardMap = new Map<string, string>();
677
+ const duplicateAnalysisMap = new Map<string, string>();
678
+ const duplicateExtensionMap = new Map<string, string>();
679
+
680
+ for (const group of dashboardDuplicates) {
681
+ const canonical = oldest(group);
682
+ for (const row of group) {
683
+ if (row.id !== canonical.id)
684
+ duplicateDashboardMap.set(row.id, canonical.id);
685
+ }
686
+ }
687
+ for (const group of analysisDuplicates) {
688
+ const canonical = oldest(group);
689
+ for (const row of group) {
690
+ if (row.id !== canonical.id)
691
+ duplicateAnalysisMap.set(row.id, canonical.id);
692
+ }
693
+ }
694
+ for (const group of extensionDuplicates) {
695
+ const canonical = oldest(group);
696
+ for (const row of group) {
697
+ if (row.id !== canonical.id && !state.extensionDataIds.has(row.id)) {
698
+ duplicateExtensionMap.set(row.id, canonical.id);
699
+ }
700
+ }
701
+ }
702
+
703
+ if (options.dryRun) {
704
+ summary.duplicateDashboardsArchived = duplicateDashboardMap.size;
705
+ summary.duplicateAnalysesHidden = duplicateAnalysisMap.size;
706
+ summary.duplicateExtensionsArchived = duplicateExtensionMap.size;
707
+ const existingMigrationKeys = new Set(
708
+ state.dashboards
709
+ .map((dashboard) => sourceMigrationKey(dashboard.config))
710
+ .filter((key): key is string => Boolean(key)),
711
+ );
712
+ summary.analysisDashboardsCreated = state.analyses.filter(
713
+ (row) =>
714
+ !duplicateAnalysisMap.has(row.id) &&
715
+ !existingMigrationKeys.has(`analysis:${row.id}`),
716
+ ).length;
717
+ const referencedExtensionIds = state.dashboards.reduce(
718
+ (ids, dashboard) => collectExtensionIds(dashboard.config, ids),
719
+ new Set<string>(),
720
+ );
721
+ summary.extensionDashboardsCreated = state.extensions.filter(
722
+ (row) =>
723
+ !row.archivedAt &&
724
+ !duplicateExtensionMap.has(row.id) &&
725
+ !existingMigrationKeys.has(`extension:${row.id}`) &&
726
+ !referencedExtensionIds.has(row.id),
727
+ ).length;
728
+ summary.dashboardsCreated =
729
+ summary.analysisDashboardsCreated + summary.extensionDashboardsCreated;
730
+ return summary;
731
+ }
732
+
733
+ const db = getDb() as any;
734
+ const runId = `artifact-migration-${Date.now()}`;
735
+ const now = nowIso();
736
+ const publicAnalyses = state.analyses.filter(
737
+ (analysis) => analysis.visibility === "public",
738
+ );
739
+ const publicExtensions = state.extensions.filter(
740
+ (extension) => extension.visibility === "public",
741
+ );
742
+ if (publicAnalyses.length > 0 || publicExtensions.length > 0) {
743
+ const blocked = [
744
+ ...publicAnalyses.map((analysis) => `analysis:${analysis.id}`),
745
+ ...publicExtensions.map((extension) => `extension:${extension.id}`),
746
+ ];
747
+ throw new Error(
748
+ `Refusing to migrate public analytics artifacts because embedded extensions cannot be public: ${blocked.join(", ")}. Make those artifacts organization-visible first, then retry.`,
749
+ );
750
+ }
751
+ await db.transaction(async (tx: any) => {
752
+ for (const row of state.dashboards) {
753
+ await tx
754
+ .insert(schema.dashboards)
755
+ .values({
756
+ id: row.id,
757
+ kind: row.kind,
758
+ title: row.title,
759
+ config: JSON.stringify(row.config),
760
+ ownerEmail: row.ownerEmail,
761
+ orgId: row.orgId,
762
+ visibility: row.visibility,
763
+ createdAt: row.createdAt,
764
+ updatedAt: row.updatedAt,
765
+ updatedBy: ctx.userEmail,
766
+ archivedAt: row.archivedAt,
767
+ hiddenAt: row.hiddenAt,
768
+ hiddenBy: null,
769
+ })
770
+ .onConflictDoNothing();
771
+ }
772
+ for (const row of state.analyses) {
773
+ await tx
774
+ .insert(schema.analyses)
775
+ .values({
776
+ id: row.id,
777
+ name: row.name,
778
+ description: row.description,
779
+ question: row.question,
780
+ instructions: row.instructions,
781
+ dataSources: JSON.stringify(row.dataSources),
782
+ resultMarkdown: row.resultMarkdown,
783
+ resultData: row.resultData ? JSON.stringify(row.resultData) : null,
784
+ author: row.author,
785
+ ownerEmail: row.ownerEmail,
786
+ orgId: row.orgId,
787
+ visibility: row.visibility,
788
+ createdAt: row.createdAt,
789
+ updatedAt: row.updatedAt,
790
+ hiddenAt: row.hiddenAt,
791
+ hiddenBy: null,
792
+ })
793
+ .onConflictDoNothing();
794
+ }
795
+
796
+ for (const [duplicateId, canonicalId] of duplicateDashboardMap) {
797
+ await copySharesBatch(
798
+ tx,
799
+ schema.dashboardShares,
800
+ schema.dashboardShares,
801
+ [{ sourceId: duplicateId, targetId: canonicalId }],
802
+ runId,
803
+ );
804
+ await tx
805
+ .update(schema.dashboardViews)
806
+ .set({ dashboardId: canonicalId })
807
+ .where(eq(schema.dashboardViews.dashboardId, duplicateId));
808
+ await tx
809
+ .update(schema.dashboardReportSubscriptions)
810
+ .set({ dashboardId: canonicalId, updatedAt: now })
811
+ .where(
812
+ eq(schema.dashboardReportSubscriptions.dashboardId, duplicateId),
813
+ );
814
+ await tx
815
+ .update(schema.dashboards)
816
+ .set({ archivedAt: now, updatedAt: now, updatedBy: ctx.userEmail })
817
+ .where(eq(schema.dashboards.id, duplicateId));
818
+ summary.duplicateDashboardsArchived += 1;
819
+ }
820
+
821
+ for (const [duplicateId, canonicalId] of duplicateAnalysisMap) {
822
+ await copySharesBatch(
823
+ tx,
824
+ schema.analysisShares,
825
+ schema.analysisShares,
826
+ [{ sourceId: duplicateId, targetId: canonicalId }],
827
+ runId,
828
+ );
829
+ await tx
830
+ .update(schema.analyses)
831
+ .set({ hiddenAt: now, hiddenBy: ctx.userEmail, updatedAt: now })
832
+ .where(eq(schema.analyses.id, duplicateId));
833
+ summary.duplicateAnalysesHidden += 1;
834
+ }
835
+
836
+ for (const [duplicateId, canonicalId] of duplicateExtensionMap) {
837
+ await copySharesBatch(
838
+ tx,
839
+ migrationExtensionShares,
840
+ migrationExtensionShares,
841
+ [{ sourceId: duplicateId, targetId: canonicalId }],
842
+ runId,
843
+ );
844
+ for (const dashboard of state.dashboards) {
845
+ const remapped = remapExtensionIds(
846
+ dashboard.config,
847
+ new Map([[duplicateId, canonicalId]]),
848
+ );
849
+ if (!remapped.changed) continue;
850
+ await tx
851
+ .update(schema.dashboards)
852
+ .set({
853
+ config: JSON.stringify(remapped.value),
854
+ updatedAt: now,
855
+ updatedBy: ctx.userEmail,
856
+ })
857
+ .where(eq(schema.dashboards.id, dashboard.id));
858
+ summary.dashboardReferencesRewritten += 1;
859
+ }
860
+ await tx
861
+ .update(migrationExtensions)
862
+ .set({ archivedAt: now, updatedAt: now })
863
+ .where(eq(migrationExtensions.id, duplicateId));
864
+ summary.duplicateExtensionsArchived += 1;
865
+ }
866
+
867
+ const extensionReplacements = duplicateExtensionMap;
868
+ const currentDashboards = state.dashboards.filter(
869
+ (row) => !duplicateDashboardMap.has(row.id) && !row.archivedAt,
870
+ );
871
+ const referencedExtensionIds = new Set<string>();
872
+ for (const dashboard of currentDashboards) {
873
+ const remapped = remapExtensionIds(
874
+ dashboard.config,
875
+ extensionReplacements,
876
+ );
877
+ collectExtensionIds(remapped.value, referencedExtensionIds);
878
+ }
879
+
880
+ for (const analysis of state.analyses) {
881
+ if (duplicateAnalysisMap.has(analysis.id)) continue;
882
+ const dashboardId = `dashboard-from-analysis-${analysis.id}`;
883
+ const extensionId = `extension-from-analysis-${analysis.id}`;
884
+ const dashboardExists = await tx
885
+ .select({ id: schema.dashboards.id })
886
+ .from(schema.dashboards)
887
+ .where(eq(schema.dashboards.id, dashboardId));
888
+ if (dashboardExists.length === 0) {
889
+ await tx
890
+ .insert(migrationExtensions)
891
+ .values({
892
+ id: extensionId,
893
+ name: `Dashboard content: ${analysis.name}`,
894
+ description:
895
+ "Hidden implementation block for a migrated dashboard.",
896
+ content: analysisExtensionContent(analysis),
897
+ icon: null,
898
+ createdAt: analysis.createdAt,
899
+ updatedAt: analysis.updatedAt,
900
+ archivedAt: null,
901
+ hiddenAt: now,
902
+ hiddenBy: ctx.userEmail,
903
+ ownerEmail: analysis.ownerEmail,
904
+ orgId: analysis.orgId,
905
+ visibility:
906
+ analysis.visibility === "public" ? "org" : analysis.visibility,
907
+ })
908
+ .onConflictDoNothing();
909
+ await copySharesBatch(
910
+ tx,
911
+ schema.analysisShares,
912
+ migrationExtensionShares,
913
+ [{ sourceId: analysis.id, targetId: extensionId }],
914
+ runId,
915
+ );
916
+ await tx.insert(schema.dashboards).values({
917
+ id: dashboardId,
918
+ kind: "sql",
919
+ title: analysis.name,
920
+ config: JSON.stringify(
921
+ migratedDashboardConfig(
922
+ analysis.name,
923
+ analysis.description,
924
+ extensionId,
925
+ "analysis",
926
+ analysis.id,
927
+ ),
928
+ ),
929
+ ownerEmail: analysis.ownerEmail,
930
+ orgId: analysis.orgId,
931
+ visibility: analysis.visibility,
932
+ createdAt: analysis.createdAt,
933
+ updatedAt: now,
934
+ updatedBy: ctx.userEmail,
935
+ hiddenAt: analysis.hiddenAt,
936
+ hiddenBy: analysis.hiddenAt ? ctx.userEmail : null,
937
+ });
938
+ await copySharesBatch(
939
+ tx,
940
+ schema.analysisShares,
941
+ schema.dashboardShares,
942
+ [{ sourceId: analysis.id, targetId: dashboardId }],
943
+ runId,
944
+ );
945
+ summary.analysisDashboardsCreated += 1;
946
+ summary.dashboardsCreated += 1;
947
+ }
948
+ await tx
949
+ .update(schema.analyses)
950
+ .set({
951
+ hiddenAt: analysis.hiddenAt ?? now,
952
+ hiddenBy: ctx.userEmail,
953
+ updatedAt: now,
954
+ })
955
+ .where(eq(schema.analyses.id, analysis.id));
956
+ if (!analysis.hiddenAt) summary.analysesHidden += 1;
957
+ }
958
+
959
+ for (const extension of state.extensions) {
960
+ if (extension.archivedAt || duplicateExtensionMap.has(extension.id))
961
+ continue;
962
+ if (referencedExtensionIds.has(extension.id)) {
963
+ await tx
964
+ .update(migrationExtensions)
965
+ .set({
966
+ hiddenAt: extension.hiddenAt ?? now,
967
+ hiddenBy: ctx.userEmail,
968
+ updatedAt: now,
969
+ })
970
+ .where(eq(migrationExtensions.id, extension.id));
971
+ if (!extension.hiddenAt) summary.extensionsHidden += 1;
972
+ continue;
973
+ }
974
+ const dashboardId = `dashboard-from-extension-${extension.id}`;
975
+ const existing = await tx
976
+ .select({ id: schema.dashboards.id })
977
+ .from(schema.dashboards)
978
+ .where(eq(schema.dashboards.id, dashboardId));
979
+ if (existing.length === 0) {
980
+ await tx.insert(schema.dashboards).values({
981
+ id: dashboardId,
982
+ kind: "sql",
983
+ title: extension.name,
984
+ config: JSON.stringify(
985
+ migratedDashboardConfig(
986
+ extension.name,
987
+ extension.description,
988
+ extension.id,
989
+ "extension",
990
+ extension.id,
991
+ ),
992
+ ),
993
+ ownerEmail: extension.ownerEmail,
994
+ orgId: extension.orgId,
995
+ visibility: extension.visibility,
996
+ createdAt: extension.createdAt,
997
+ updatedAt: now,
998
+ updatedBy: ctx.userEmail,
999
+ hiddenAt: extension.hiddenAt,
1000
+ hiddenBy: extension.hiddenAt ? ctx.userEmail : null,
1001
+ });
1002
+ await copySharesBatch(
1003
+ tx,
1004
+ migrationExtensionShares,
1005
+ schema.dashboardShares,
1006
+ [{ sourceId: extension.id, targetId: dashboardId }],
1007
+ runId,
1008
+ );
1009
+ summary.extensionDashboardsCreated += 1;
1010
+ summary.dashboardsCreated += 1;
1011
+ }
1012
+ await tx
1013
+ .update(migrationExtensions)
1014
+ .set({
1015
+ hiddenAt: extension.hiddenAt ?? now,
1016
+ hiddenBy: ctx.userEmail,
1017
+ updatedAt: now,
1018
+ })
1019
+ .where(eq(migrationExtensions.id, extension.id));
1020
+ if (!extension.hiddenAt) summary.extensionsHidden += 1;
1021
+ }
1022
+
1023
+ const legacyKeys = [
1024
+ ...state.legacyDashboardKeys,
1025
+ ...state.legacyAnalysisKeys,
1026
+ ].map((key) => `o:${ctx.orgId}:${key}`);
1027
+ for (let offset = 0; offset < legacyKeys.length; offset += 500) {
1028
+ const chunk = legacyKeys.slice(offset, offset + 500);
1029
+ if (chunk.length === 0) continue;
1030
+ await tx
1031
+ .delete(migrationSettings)
1032
+ .where(inArray(migrationSettings.key, chunk));
1033
+ summary.legacySettingsDeleted += chunk.length;
1034
+ }
1035
+ });
1036
+
1037
+ recordChange({
1038
+ source: "dashboards",
1039
+ type: "change",
1040
+ key: ctx.orgId,
1041
+ orgId: ctx.orgId,
1042
+ });
1043
+ recordChange({
1044
+ source: "analyses",
1045
+ type: "change",
1046
+ key: ctx.orgId,
1047
+ orgId: ctx.orgId,
1048
+ });
1049
+ recordChange({
1050
+ source: "extensions",
1051
+ type: "change",
1052
+ key: ctx.orgId,
1053
+ orgId: ctx.orgId,
1054
+ });
1055
+ return summary;
1056
+ }
1057
+
1058
+ export async function analyticsArtifactMigrationInventory(
1059
+ ctx: AnalyticsArtifactMigrationContext,
1060
+ ): Promise<AnalyticsArtifactMigrationSummary> {
1061
+ return migrateAnalyticsArtifacts(ctx, { dryRun: true });
1062
+ }