@agent-native/core 0.114.12 → 0.114.15

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 (241) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +21 -0
  3. package/corpus/core/docs/content/deployment.mdx +1 -1
  4. package/corpus/core/docs/content/frames.mdx +1 -1
  5. package/corpus/core/docs/content/integrations.mdx +27 -27
  6. package/corpus/core/docs/content/pr-visual-recap.mdx +17 -13
  7. package/corpus/core/docs/content/workspace-management.mdx +1 -1
  8. package/corpus/core/package.json +2 -1
  9. package/corpus/core/src/agent/engine/builder-engine.ts +4 -0
  10. package/corpus/core/src/application-state/index.ts +3 -0
  11. package/corpus/core/src/application-state/script-helpers.ts +12 -1
  12. package/corpus/core/src/application-state/store.ts +216 -15
  13. package/corpus/core/src/client/AgentPanel.tsx +10 -4
  14. package/corpus/core/src/client/ConnectBuilderCard.tsx +5 -1
  15. package/corpus/core/src/client/chat/markdown-renderer.tsx +3 -5
  16. package/corpus/core/src/client/error-format.ts +2 -1
  17. package/corpus/core/src/client/index.ts +1 -0
  18. package/corpus/core/src/client/integrations/IntegrationsPanel.tsx +2 -18
  19. package/corpus/core/src/client/resources/McpIntegrationDialog.tsx +43 -32
  20. package/corpus/core/src/client/resources/mcp-integration-catalog.ts +4 -2
  21. package/corpus/core/src/client/settings/BackgroundAgentSection.tsx +5 -1
  22. package/corpus/core/src/client/settings/SettingsPanel.tsx +4 -4
  23. package/corpus/core/src/client/settings/useBuilderStatus.ts +3 -0
  24. package/corpus/core/src/db/client.ts +44 -1
  25. package/corpus/core/src/localization/default-messages.ts +1 -7
  26. package/corpus/core/src/server/builder-browser.ts +4 -0
  27. package/corpus/core/src/server/builder-design-systems.ts +6 -1
  28. package/corpus/core/src/server/fusion-app.ts +5 -1
  29. package/corpus/core/src/shared/builder-link-tracking.ts +41 -0
  30. package/corpus/core/src/shared/index.ts +1 -0
  31. package/corpus/templates/analytics/AGENTS.md +10 -1
  32. package/corpus/templates/analytics/actions/save-dashboard-report-subscription.ts +10 -2
  33. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +5 -4
  34. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +20 -32
  35. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/report-panel-window.ts +62 -0
  36. package/corpus/templates/analytics/changelog/2026-07-21-chart-series-controls-stay-open-while-moving-from-the-legend.md +6 -0
  37. package/corpus/templates/analytics/changelog/2026-07-21-dashboard-email-captures-complete-large-dashboards-in-chunks.md +6 -0
  38. package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +4 -4
  39. package/corpus/templates/analytics/server/jobs/dashboard-report.ts +1 -8
  40. package/corpus/templates/analytics/server/lib/dashboard-report-subscriptions.ts +39 -7
  41. package/corpus/templates/analytics/server/lib/dashboard-report.ts +341 -251
  42. package/corpus/templates/analytics/server/lib/first-party-dashboard-repair.ts +114 -0
  43. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +84 -6
  44. package/corpus/templates/analytics/server/plugins/db.ts +13 -0
  45. package/corpus/templates/clips/app/components/player/video-player.tsx +10 -4
  46. package/corpus/templates/clips/changelog/2026-07-21-choose-which-whisper-model-to-use-in-settings.md +6 -0
  47. package/corpus/templates/clips/changelog/2026-07-21-fixed-organization-recording-visibility-default.md +6 -0
  48. package/corpus/templates/clips/changelog/2026-07-21-live-dictation-text-now-appears-above-the-desktop-recording-.md +6 -0
  49. package/corpus/templates/clips/changelog/2026-07-21-meeting-notes-stop-when-calls-end.md +6 -0
  50. package/corpus/templates/clips/desktop/src/app.tsx +66 -75
  51. package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +2 -0
  52. package/corpus/templates/clips/desktop/src/hooks/useWhisperSettings.ts +172 -0
  53. package/corpus/templates/clips/desktop/src/lib/meeting-call-app.ts +41 -0
  54. package/corpus/templates/clips/desktop/src/lib/silence-events.ts +1 -0
  55. package/corpus/templates/clips/desktop/src/lib/voice-dictation.ts +14 -3
  56. package/corpus/templates/clips/desktop/src/overlays/flow-bar.tsx +25 -0
  57. package/corpus/templates/clips/desktop/src/shared/config.ts +1 -0
  58. package/corpus/templates/clips/desktop/src/styles.css +100 -7
  59. package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +3 -4
  60. package/corpus/templates/clips/desktop/src-tauri/src/config.rs +32 -0
  61. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +3 -0
  62. package/corpus/templates/clips/desktop/src-tauri/src/native_screen/custom_capture.rs +11 -4
  63. package/corpus/templates/clips/desktop/src-tauri/src/silence_detector.rs +268 -20
  64. package/corpus/templates/clips/desktop/src-tauri/src/whisper_model.rs +285 -164
  65. package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +18 -7
  66. package/corpus/templates/clips/learnings.defaults.md +1 -1
  67. package/corpus/templates/clips/server/plugins/db.ts +11 -0
  68. package/corpus/templates/clips/shared/builder-credits.ts +1 -1
  69. package/corpus/templates/design/AGENTS.md +7 -3
  70. package/corpus/templates/design/actions/begin-node-rewrite-request.ts +78 -0
  71. package/corpus/templates/design/actions/cancel-node-rewrite-request.ts +17 -7
  72. package/corpus/templates/design/actions/propose-node-rewrite.ts +26 -18
  73. package/corpus/templates/design/actions/resolve-node-rewrite.ts +67 -19
  74. package/corpus/templates/design/app/components/design/FusionAppBanner.tsx +9 -1
  75. package/corpus/templates/design/app/components/design/edit-panel/component-section.tsx +5 -1
  76. package/corpus/templates/design/app/components/visual-editor/NodeRewriteProposal.tsx +1 -4
  77. package/corpus/templates/design/app/components/visual-editor/ReviewCanvasPins.tsx +2 -6
  78. package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +9 -1
  79. package/corpus/templates/design/shared/node-rewrite.ts +32 -0
  80. package/corpus/templates/forms/app/components/layout/Sidebar.tsx +6 -3
  81. package/corpus/templates/forms/app/pages/FormBuilderPage.tsx +11 -18
  82. package/corpus/templates/forms/app/pages/FormsListPage.tsx +9 -5
  83. package/corpus/templates/slides/app/components/design-system/DesignSystemSetup.tsx +9 -1
  84. package/corpus/templates/slides/app/context/DeckContext.tsx +18 -0
  85. package/corpus/templates/slides/changelog/2026-07-21-decks-list-now-refreshes-when-you-switch-organizations.md +6 -0
  86. package/corpus/templates/tasks/app/components/custom-fields/FieldCreateBar.tsx +13 -19
  87. package/corpus/templates/tasks/app/components/custom-fields/FieldListRow.tsx +27 -22
  88. package/corpus/templates/tasks/app/components/custom-fields/FieldsList.tsx +26 -22
  89. package/corpus/templates/tasks/app/components/custom-fields/FieldsListSkeleton.tsx +4 -1
  90. package/corpus/templates/tasks/app/components/custom-fields/FieldsPage.tsx +4 -2
  91. package/corpus/templates/tasks/app/components/custom-fields/editor/FieldEditorSidebar.tsx +12 -3
  92. package/corpus/templates/tasks/app/components/custom-fields/editor/FieldTitleSection.tsx +6 -3
  93. package/corpus/templates/tasks/app/components/custom-fields/editor/config/CurrencyConfigControl.tsx +5 -2
  94. package/corpus/templates/tasks/app/components/custom-fields/editor/config/NumberConfigControl.tsx +5 -2
  95. package/corpus/templates/tasks/app/components/custom-fields/editor/config/PercentConfigControl.tsx +4 -1
  96. package/corpus/templates/tasks/app/components/custom-fields/editor/config/SelectConfigControl.tsx +17 -7
  97. package/corpus/templates/tasks/app/components/custom-fields/editor/config/select-colors.ts +37 -9
  98. package/corpus/templates/tasks/app/components/custom-fields/field-types.ts +23 -0
  99. package/corpus/templates/tasks/app/components/inbox/InboxList.tsx +24 -16
  100. package/corpus/templates/tasks/app/components/inbox/InboxListPage.tsx +5 -3
  101. package/corpus/templates/tasks/app/components/inbox/InboxListRow.tsx +7 -5
  102. package/corpus/templates/tasks/app/components/layout/Header.tsx +10 -10
  103. package/corpus/templates/tasks/app/components/layout/Layout.tsx +11 -7
  104. package/corpus/templates/tasks/app/components/layout/Sidebar.tsx +17 -10
  105. package/corpus/templates/tasks/app/components/shared/AddListItemInput.tsx +14 -8
  106. package/corpus/templates/tasks/app/components/shared/AlertDialog.tsx +6 -2
  107. package/corpus/templates/tasks/app/components/shared/BulkDeleteDialog.tsx +13 -6
  108. package/corpus/templates/tasks/app/components/shared/ChipSelect.tsx +12 -5
  109. package/corpus/templates/tasks/app/components/shared/DeleteItemDialog.tsx +6 -6
  110. package/corpus/templates/tasks/app/components/shared/ListViewHeader.tsx +3 -1
  111. package/corpus/templates/tasks/app/components/shared/SidePanel.tsx +5 -2
  112. package/corpus/templates/tasks/app/components/shared/list/ListRowDragHandle.tsx +3 -1
  113. package/corpus/templates/tasks/app/components/shared/selection/ListSelectionBar.tsx +46 -20
  114. package/corpus/templates/tasks/app/components/shared/selection/ListSelectionHeaderToggle.tsx +3 -1
  115. package/corpus/templates/tasks/app/components/shared/selection/ListSelectionToolbar.tsx +10 -6
  116. package/corpus/templates/tasks/app/components/tasks/TaskList.tsx +24 -13
  117. package/corpus/templates/tasks/app/components/tasks/TaskListHeaderRow.tsx +4 -1
  118. package/corpus/templates/tasks/app/components/tasks/TaskListPage.tsx +5 -3
  119. package/corpus/templates/tasks/app/components/tasks/TaskListRow.tsx +15 -6
  120. package/corpus/templates/tasks/app/components/tasks/fields/TaskFieldsSidebar.tsx +11 -6
  121. package/corpus/templates/tasks/app/components/tasks/fields/TaskTitleSection.tsx +8 -3
  122. package/corpus/templates/tasks/app/components/tasks/fields/controls/RichTextValueControl.tsx +12 -6
  123. package/corpus/templates/tasks/app/components/tasks/fields/controls/SingleSelectValueControl.tsx +6 -1
  124. package/corpus/templates/tasks/app/i18n/en-US.ts +209 -0
  125. package/corpus/templates/tasks/app/i18n/index.ts +34 -0
  126. package/corpus/templates/tasks/app/root.tsx +34 -2
  127. package/corpus/templates/tasks/app/routes/extensions.$id.tsx +2 -1
  128. package/corpus/templates/tasks/app/routes/extensions._index.tsx +2 -1
  129. package/corpus/templates/tasks/app/routes/fields.tsx +2 -1
  130. package/corpus/templates/tasks/app/routes/inbox.tsx +2 -1
  131. package/corpus/templates/tasks/app/routes/tasks.tsx +2 -1
  132. package/corpus/templates/tasks/app/routes/team.tsx +6 -3
  133. package/corpus/templates/tasks/changelog/2026-07-20-tasks-is-now-available-in-10-additional-languages.md +6 -0
  134. package/dist/agent/engine/builder-engine.d.ts.map +1 -1
  135. package/dist/agent/engine/builder-engine.js +4 -0
  136. package/dist/agent/engine/builder-engine.js.map +1 -1
  137. package/dist/application-state/index.d.ts +2 -2
  138. package/dist/application-state/index.d.ts.map +1 -1
  139. package/dist/application-state/index.js +2 -2
  140. package/dist/application-state/index.js.map +1 -1
  141. package/dist/application-state/script-helpers.d.ts +3 -1
  142. package/dist/application-state/script-helpers.d.ts.map +1 -1
  143. package/dist/application-state/script-helpers.js +7 -1
  144. package/dist/application-state/script-helpers.js.map +1 -1
  145. package/dist/application-state/store.d.ts +7 -1
  146. package/dist/application-state/store.d.ts.map +1 -1
  147. package/dist/application-state/store.js +143 -17
  148. package/dist/application-state/store.js.map +1 -1
  149. package/dist/client/AgentPanel.d.ts.map +1 -1
  150. package/dist/client/AgentPanel.js +10 -3
  151. package/dist/client/AgentPanel.js.map +1 -1
  152. package/dist/client/ConnectBuilderCard.d.ts.map +1 -1
  153. package/dist/client/ConnectBuilderCard.js +5 -1
  154. package/dist/client/ConnectBuilderCard.js.map +1 -1
  155. package/dist/client/chat/markdown-renderer.d.ts.map +1 -1
  156. package/dist/client/chat/markdown-renderer.js +3 -2
  157. package/dist/client/chat/markdown-renderer.js.map +1 -1
  158. package/dist/client/error-format.d.ts +1 -1
  159. package/dist/client/error-format.d.ts.map +1 -1
  160. package/dist/client/error-format.js +1 -1
  161. package/dist/client/error-format.js.map +1 -1
  162. package/dist/client/index.d.ts +1 -0
  163. package/dist/client/index.d.ts.map +1 -1
  164. package/dist/client/index.js +1 -0
  165. package/dist/client/index.js.map +1 -1
  166. package/dist/client/integrations/IntegrationsPanel.d.ts.map +1 -1
  167. package/dist/client/integrations/IntegrationsPanel.js +3 -4
  168. package/dist/client/integrations/IntegrationsPanel.js.map +1 -1
  169. package/dist/client/resources/McpIntegrationDialog.d.ts.map +1 -1
  170. package/dist/client/resources/McpIntegrationDialog.js +21 -7
  171. package/dist/client/resources/McpIntegrationDialog.js.map +1 -1
  172. package/dist/client/resources/mcp-integration-catalog.d.ts.map +1 -1
  173. package/dist/client/resources/mcp-integration-catalog.js +2 -2
  174. package/dist/client/resources/mcp-integration-catalog.js.map +1 -1
  175. package/dist/client/settings/BackgroundAgentSection.d.ts.map +1 -1
  176. package/dist/client/settings/BackgroundAgentSection.js +5 -1
  177. package/dist/client/settings/BackgroundAgentSection.js.map +1 -1
  178. package/dist/client/settings/SettingsPanel.js +1 -1
  179. package/dist/client/settings/SettingsPanel.js.map +1 -1
  180. package/dist/client/settings/useBuilderStatus.d.ts.map +1 -1
  181. package/dist/client/settings/useBuilderStatus.js +2 -0
  182. package/dist/client/settings/useBuilderStatus.js.map +1 -1
  183. package/dist/collab/routes.d.ts +1 -1
  184. package/dist/db/client.d.ts +7 -0
  185. package/dist/db/client.d.ts.map +1 -1
  186. package/dist/db/client.js +39 -1
  187. package/dist/db/client.js.map +1 -1
  188. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  189. package/dist/localization/default-messages.d.ts +0 -6
  190. package/dist/localization/default-messages.d.ts.map +1 -1
  191. package/dist/localization/default-messages.js +1 -7
  192. package/dist/localization/default-messages.js.map +1 -1
  193. package/dist/notifications/routes.d.ts +2 -2
  194. package/dist/observability/routes.d.ts +3 -3
  195. package/dist/resources/handlers.d.ts +1 -1
  196. package/dist/server/builder-browser.d.ts.map +1 -1
  197. package/dist/server/builder-browser.js +4 -0
  198. package/dist/server/builder-browser.js.map +1 -1
  199. package/dist/server/builder-design-systems.d.ts.map +1 -1
  200. package/dist/server/builder-design-systems.js +6 -1
  201. package/dist/server/builder-design-systems.js.map +1 -1
  202. package/dist/server/fusion-app.d.ts.map +1 -1
  203. package/dist/server/fusion-app.js +2 -1
  204. package/dist/server/fusion-app.js.map +1 -1
  205. package/dist/server/transcribe-voice.d.ts +1 -1
  206. package/dist/shared/builder-link-tracking.d.ts +14 -0
  207. package/dist/shared/builder-link-tracking.d.ts.map +1 -0
  208. package/dist/shared/builder-link-tracking.js +26 -0
  209. package/dist/shared/builder-link-tracking.js.map +1 -0
  210. package/dist/shared/index.d.ts +1 -0
  211. package/dist/shared/index.d.ts.map +1 -1
  212. package/dist/shared/index.js +1 -0
  213. package/dist/shared/index.js.map +1 -1
  214. package/docs/content/deployment.mdx +1 -1
  215. package/docs/content/frames.mdx +1 -1
  216. package/docs/content/integrations.mdx +27 -27
  217. package/docs/content/pr-visual-recap.mdx +17 -13
  218. package/docs/content/workspace-management.mdx +1 -1
  219. package/package.json +3 -2
  220. package/src/agent/engine/builder-engine.ts +4 -0
  221. package/src/application-state/index.ts +3 -0
  222. package/src/application-state/script-helpers.ts +12 -1
  223. package/src/application-state/store.ts +216 -15
  224. package/src/client/AgentPanel.tsx +10 -4
  225. package/src/client/ConnectBuilderCard.tsx +5 -1
  226. package/src/client/chat/markdown-renderer.tsx +3 -5
  227. package/src/client/error-format.ts +2 -1
  228. package/src/client/index.ts +1 -0
  229. package/src/client/integrations/IntegrationsPanel.tsx +2 -18
  230. package/src/client/resources/McpIntegrationDialog.tsx +43 -32
  231. package/src/client/resources/mcp-integration-catalog.ts +4 -2
  232. package/src/client/settings/BackgroundAgentSection.tsx +5 -1
  233. package/src/client/settings/SettingsPanel.tsx +4 -4
  234. package/src/client/settings/useBuilderStatus.ts +3 -0
  235. package/src/db/client.ts +44 -1
  236. package/src/localization/default-messages.ts +1 -7
  237. package/src/server/builder-browser.ts +4 -0
  238. package/src/server/builder-design-systems.ts +6 -1
  239. package/src/server/fusion-app.ts +5 -1
  240. package/src/shared/builder-link-tracking.ts +41 -0
  241. package/src/shared/index.ts +1 -0
@@ -148,6 +148,11 @@ import { dashboardExtensionSlotId } from "./extension-slot";
148
148
  import { interpolate } from "./interpolate";
149
149
  import { serializePanelSql } from "./panel-sql";
150
150
  import { AddPanelPopover, PanelEditorDialog } from "./PanelEditorDialog";
151
+ import {
152
+ listReportablePanelIds,
153
+ parseReportPanelWindow,
154
+ windowReportPanels,
155
+ } from "./report-panel-window";
151
156
  import { SqlChartCard } from "./SqlChartCard";
152
157
  import {
153
158
  clampDashboardColumns,
@@ -188,31 +193,6 @@ function groupDashboardTabs(tabs: string[]): {
188
193
  return { groups, hasNestedTabs };
189
194
  }
190
195
 
191
- function parseReportPanelLimit(raw: string | null): number | null {
192
- if (!raw) return null;
193
- const parsed = Number.parseInt(raw, 10);
194
- if (!Number.isFinite(parsed) || parsed <= 0) return null;
195
- return Math.min(50, parsed);
196
- }
197
-
198
- function limitReportPanels(panels: SqlPanel[], limit: number | null) {
199
- if (!limit) return panels;
200
- const limited: SqlPanel[] = [];
201
- let chartCount = 0;
202
-
203
- for (const panel of panels) {
204
- if (panel.chartType === "section") {
205
- if (chartCount < limit) limited.push(panel);
206
- continue;
207
- }
208
- if (chartCount >= limit) continue;
209
- chartCount++;
210
- limited.push(panel);
211
- }
212
-
213
- return limited;
214
- }
215
-
216
196
  function DashboardDropLine({
217
197
  slot,
218
198
  activeSlot,
@@ -504,8 +484,11 @@ export default function SqlDashboardPage() {
504
484
  const dashboardId = searchParams.get("id") || routeId;
505
485
  const reportScreenshot = searchParams.get("reportScreenshot") === "1";
506
486
  const reportSettingsRequested = searchParams.get("reportSettings") === "1";
507
- const reportPanelLimit = reportScreenshot
508
- ? parseReportPanelLimit(searchParams.get("reportPanelLimit"))
487
+ const reportPanelWindow = reportScreenshot
488
+ ? parseReportPanelWindow(
489
+ searchParams.get("reportPanelOffset"),
490
+ searchParams.get("reportPanelLimit"),
491
+ )
509
492
  : null;
510
493
 
511
494
  const [dashboard, setDashboard] = useState<SqlDashboardConfig | null>(null);
@@ -1180,9 +1163,9 @@ export default function SqlDashboardPage() {
1180
1163
  ? requestedTab
1181
1164
  : tabs[0]
1182
1165
  : null;
1183
- // Report captures need the complete dashboard in one image. The report
1184
- // URL intentionally has no `tab` parameter, so do not apply the normal
1185
- // first-tab selection while rendering the screenshot surface.
1166
+ // Report captures cover the complete dashboard across ordered windows. The
1167
+ // report URL intentionally has no `tab` parameter, so do not apply the
1168
+ // normal first-tab selection while rendering the screenshot surface.
1186
1169
  const activeTab = reportScreenshot ? null : selectedTab;
1187
1170
  const groupedTabs = useMemo(() => groupDashboardTabs(tabs), [tabs]);
1188
1171
  const activeTabGroup = activeTab
@@ -1221,8 +1204,8 @@ export default function SqlDashboardPage() {
1221
1204
  const tabPanels = activeTab
1222
1205
  ? dashboard.panels.filter((p) => !p.tab || p.tab === activeTab)
1223
1206
  : dashboard.panels;
1224
- return limitReportPanels(tabPanels, reportPanelLimit);
1225
- }, [dashboard, activeTab, reportPanelLimit]);
1207
+ return windowReportPanels(tabPanels, reportPanelWindow);
1208
+ }, [dashboard, activeTab, reportPanelWindow]);
1226
1209
 
1227
1210
  // Group panels into "section blocks": each section starts a new block whose
1228
1211
  // grid uses the section's `columns` (falling back to the dashboard default).
@@ -1713,6 +1696,11 @@ export default function SqlDashboardPage() {
1713
1696
  className="space-y-4"
1714
1697
  data-dashboard-report-capture
1715
1698
  data-dashboard-report-ready={loaded && dashboard ? "true" : "false"}
1699
+ data-dashboard-report-panel-ids={
1700
+ reportScreenshot
1701
+ ? JSON.stringify(listReportablePanelIds(visiblePanels))
1702
+ : undefined
1703
+ }
1716
1704
  >
1717
1705
  {hiddenAt ? (
1718
1706
  <div className="flex flex-wrap items-center gap-3 rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-sm text-amber-900 dark:border-amber-900/70 dark:bg-amber-950/40 dark:text-amber-200">
@@ -0,0 +1,62 @@
1
+ import type { SqlPanel } from "./types";
2
+
3
+ export const REPORT_PANEL_CHUNK_SIZE = 8;
4
+
5
+ export function listReportablePanelIds(panels: SqlPanel[]): string[] {
6
+ return panels
7
+ .filter((panel) => panel.chartType !== "section")
8
+ .map((panel) => panel.id);
9
+ }
10
+
11
+ export function parseReportPanelWindow(
12
+ offsetRaw: string | null,
13
+ limitRaw: string | null,
14
+ ): { offset: number; limit: number } | null {
15
+ if (!offsetRaw || !limitRaw) return null;
16
+ const offset = Number.parseInt(offsetRaw, 10);
17
+ const limit = Number.parseInt(limitRaw, 10);
18
+ if (!Number.isFinite(offset) || offset < 0) return null;
19
+ if (!Number.isFinite(limit) || limit <= 0) return null;
20
+ return { offset, limit: Math.min(REPORT_PANEL_CHUNK_SIZE, limit) };
21
+ }
22
+
23
+ export function countReportablePanels(panels: SqlPanel[]): number {
24
+ return listReportablePanelIds(panels).length;
25
+ }
26
+
27
+ /**
28
+ * A report chunk is indexed by chart panels, not section headers. When a
29
+ * chunk starts inside a section, repeat that section header so each image is
30
+ * readable on its own while preserving the dashboard's panel order.
31
+ */
32
+ export function windowReportPanels(
33
+ panels: SqlPanel[],
34
+ window: { offset: number; limit: number } | null,
35
+ ): SqlPanel[] {
36
+ if (!window) return panels;
37
+
38
+ const end = window.offset + window.limit;
39
+ let chartIndex = 0;
40
+ let activeSection: SqlPanel | null = null;
41
+ let includedFirstChart = false;
42
+ const selected: SqlPanel[] = [];
43
+
44
+ for (const panel of panels) {
45
+ if (panel.chartType === "section") {
46
+ activeSection = panel;
47
+ if (includedFirstChart && chartIndex < end) selected.push(panel);
48
+ continue;
49
+ }
50
+
51
+ const include = chartIndex >= window.offset && chartIndex < end;
52
+ if (include) {
53
+ if (!includedFirstChart && activeSection) selected.push(activeSection);
54
+ selected.push(panel);
55
+ includedFirstChart = true;
56
+ }
57
+ chartIndex++;
58
+ if (chartIndex >= end) break;
59
+ }
60
+
61
+ return selected;
62
+ }
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-21
4
+ ---
5
+
6
+ Chart series controls stay open while moving from the legend
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-21
4
+ ---
5
+
6
+ Daily dashboard emails now include every panel reliably, even for large dashboards.
@@ -480,7 +480,7 @@
480
480
  "chartType": "area",
481
481
  "source": "first-party",
482
482
  "width": 2,
483
- "sql": "WITH all_users AS (SELECT NULLIF(user_key, '') AS user_key, event_date, user_id, COALESCE(NULLIF(template, ''), NULLIF(properties::jsonb ->> 'templateId', ''), NULLIF(properties::jsonb ->> 'agent_native_template', ''), NULLIF(properties::jsonb ->> 'agentNativeTemplate', ''), NULLIF(app, ''), NULLIF(properties::jsonb ->> 'agent_native_app', ''), NULLIF(properties::jsonb ->> 'agentNativeApp', ''), 'unknown') AS template FROM analytics_events WHERE event_name = 'session status' AND signed_in = 'true' AND NULLIF(user_key, '') IS NOT NULL AND lower(COALESCE(NULLIF(template, ''), NULLIF(properties::jsonb ->> 'templateId', ''), NULLIF(properties::jsonb ->> 'agent_native_template', ''), NULLIF(properties::jsonb ->> 'agentNativeTemplate', ''), NULLIF(app, ''), NULLIF(properties::jsonb ->> 'agent_native_app', ''), NULLIF(properties::jsonb ->> 'agentNativeApp', ''), 'unknown')) <> 'docs' AND ('{{emailFilter}}' IN ('', 'all') OR ('{{emailFilter}}' = 'exclude_builder' AND lower(coalesce(user_id, '')) NOT LIKE '%@builder.io') OR ('{{emailFilter}}' = 'only_builder' AND lower(coalesce(user_id, '')) LIKE '%@builder.io'))), first_seen AS (SELECT user_key, MIN(event_date) AS first_date FROM all_users GROUP BY user_key) SELECT a.event_date AS date, a.template AS template, COUNT(DISTINCT a.user_key) AS users FROM all_users a JOIN first_seen f ON f.user_key = a.user_key WHERE a.event_date <> f.first_date AND a.template <> 'unknown' AND (a.event_date <= to_char(CURRENT_DATE, 'YYYY-MM-DD') AND ('{{timeRange}}' IN ('', 'all') OR ('{{timeRange}}' = '7d' AND a.event_date >= to_char(CURRENT_DATE - INTERVAL '7 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '30d' AND a.event_date >= to_char(CURRENT_DATE - INTERVAL '30 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '90d' AND a.event_date >= to_char(CURRENT_DATE - INTERVAL '90 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '180d' AND a.event_date >= to_char(CURRENT_DATE - INTERVAL '180 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '365d' AND a.event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD')))) GROUP BY 1, 2 ORDER BY date, template",
483
+ "sql": "WITH all_users AS (SELECT NULLIF(user_key, '') AS user_key, event_date, user_id, COALESCE(NULLIF(template, ''), NULLIF(properties::jsonb ->> 'templateId', ''), NULLIF(properties::jsonb ->> 'agent_native_template', ''), NULLIF(properties::jsonb ->> 'agentNativeTemplate', ''), NULLIF(app, ''), NULLIF(properties::jsonb ->> 'agent_native_app', ''), NULLIF(properties::jsonb ->> 'agentNativeApp', ''), 'unknown') AS template FROM analytics_events WHERE event_name = 'session status' AND signed_in = 'true' AND NULLIF(user_key, '') IS NOT NULL AND lower(COALESCE(NULLIF(template, ''), NULLIF(properties::jsonb ->> 'templateId', ''), NULLIF(properties::jsonb ->> 'agent_native_template', ''), NULLIF(properties::jsonb ->> 'agentNativeTemplate', ''), NULLIF(app, ''), NULLIF(properties::jsonb ->> 'agent_native_app', ''), NULLIF(properties::jsonb ->> 'agentNativeApp', ''), 'unknown')) <> 'docs' AND ('{{emailFilter}}' IN ('', 'all') OR ('{{emailFilter}}' = 'exclude_builder' AND lower(coalesce(user_id, '')) NOT LIKE '%@builder.io') OR ('{{emailFilter}}' = 'only_builder' AND lower(coalesce(user_id, '')) LIKE '%@builder.io')) AND event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD')), first_seen AS (SELECT user_key, MIN(event_date) AS first_date FROM all_users GROUP BY user_key) SELECT a.event_date AS date, a.template AS template, COUNT(DISTINCT a.user_key) AS users FROM all_users a JOIN first_seen f ON f.user_key = a.user_key WHERE a.event_date <> f.first_date AND a.template <> 'unknown' AND (a.event_date <= to_char(CURRENT_DATE, 'YYYY-MM-DD') AND ('{{timeRange}}' IN ('', 'all') OR ('{{timeRange}}' = '7d' AND a.event_date >= to_char(CURRENT_DATE - INTERVAL '7 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '30d' AND a.event_date >= to_char(CURRENT_DATE - INTERVAL '30 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '90d' AND a.event_date >= to_char(CURRENT_DATE - INTERVAL '90 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '180d' AND a.event_date >= to_char(CURRENT_DATE - INTERVAL '180 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '365d' AND a.event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD')))) GROUP BY 1, 2 ORDER BY date, template",
484
484
  "config": {
485
485
  "xKey": "date",
486
486
  "yKey": "users",
@@ -491,7 +491,7 @@
491
491
  "valueKey": "users"
492
492
  },
493
493
  "stacked": true,
494
- "description": "Daily signed-in visitors who are NOT on their all-time first active day (Recurring only), stacked by inferred template/app used that day. Docs traffic and unknown template are excluded."
494
+ "description": "Daily signed-in visitors who are not on their first active day observed in the previous 365 days, stacked by inferred template/app used that day. Docs traffic and unknown template are excluded."
495
495
  }
496
496
  },
497
497
  {
@@ -500,7 +500,7 @@
500
500
  "chartType": "bar",
501
501
  "source": "first-party",
502
502
  "width": 1,
503
- "sql": "WITH all_users AS (SELECT NULLIF(user_key, '') AS user_key, event_date, user_id, COALESCE(NULLIF(template, ''), NULLIF(properties::jsonb ->> 'templateId', ''), NULLIF(properties::jsonb ->> 'agent_native_template', ''), NULLIF(properties::jsonb ->> 'agentNativeTemplate', ''), NULLIF(app, ''), NULLIF(properties::jsonb ->> 'agent_native_app', ''), NULLIF(properties::jsonb ->> 'agentNativeApp', ''), 'unknown') AS template FROM analytics_events WHERE event_name = 'session status' AND signed_in = 'true' AND NULLIF(user_key, '') IS NOT NULL AND lower(COALESCE(NULLIF(template, ''), NULLIF(properties::jsonb ->> 'templateId', ''), NULLIF(properties::jsonb ->> 'agent_native_template', ''), NULLIF(properties::jsonb ->> 'agentNativeTemplate', ''), NULLIF(app, ''), NULLIF(properties::jsonb ->> 'agent_native_app', ''), NULLIF(properties::jsonb ->> 'agentNativeApp', ''), 'unknown')) <> 'docs' AND ('{{emailFilter}}' IN ('', 'all') OR ('{{emailFilter}}' = 'exclude_builder' AND lower(coalesce(user_id, '')) NOT LIKE '%@builder.io') OR ('{{emailFilter}}' = 'only_builder' AND lower(coalesce(user_id, '')) LIKE '%@builder.io'))), first_seen AS (SELECT user_key, MIN(event_date) AS first_date FROM all_users GROUP BY user_key) SELECT to_char(date_trunc('week', a.event_date::date), 'YYYY-MM-DD') AS date, a.template AS template, COUNT(DISTINCT a.user_key) AS users FROM all_users a JOIN first_seen f ON f.user_key = a.user_key WHERE a.event_date <> f.first_date AND a.template <> 'unknown' AND (a.event_date <= to_char(CURRENT_DATE, 'YYYY-MM-DD') AND ('{{timeRange}}' IN ('', 'all') OR ('{{timeRange}}' = '7d' AND a.event_date >= to_char(CURRENT_DATE - INTERVAL '7 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '30d' AND a.event_date >= to_char(CURRENT_DATE - INTERVAL '30 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '90d' AND a.event_date >= to_char(CURRENT_DATE - INTERVAL '90 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '180d' AND a.event_date >= to_char(CURRENT_DATE - INTERVAL '180 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '365d' AND a.event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD')))) GROUP BY 1, 2 ORDER BY date, template",
503
+ "sql": "WITH all_users AS (SELECT NULLIF(user_key, '') AS user_key, event_date, user_id, COALESCE(NULLIF(template, ''), NULLIF(properties::jsonb ->> 'templateId', ''), NULLIF(properties::jsonb ->> 'agent_native_template', ''), NULLIF(properties::jsonb ->> 'agentNativeTemplate', ''), NULLIF(app, ''), NULLIF(properties::jsonb ->> 'agent_native_app', ''), NULLIF(properties::jsonb ->> 'agentNativeApp', ''), 'unknown') AS template FROM analytics_events WHERE event_name = 'session status' AND signed_in = 'true' AND NULLIF(user_key, '') IS NOT NULL AND lower(COALESCE(NULLIF(template, ''), NULLIF(properties::jsonb ->> 'templateId', ''), NULLIF(properties::jsonb ->> 'agent_native_template', ''), NULLIF(properties::jsonb ->> 'agentNativeTemplate', ''), NULLIF(app, ''), NULLIF(properties::jsonb ->> 'agent_native_app', ''), NULLIF(properties::jsonb ->> 'agentNativeApp', ''), 'unknown')) <> 'docs' AND ('{{emailFilter}}' IN ('', 'all') OR ('{{emailFilter}}' = 'exclude_builder' AND lower(coalesce(user_id, '')) NOT LIKE '%@builder.io') OR ('{{emailFilter}}' = 'only_builder' AND lower(coalesce(user_id, '')) LIKE '%@builder.io')) AND event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD')), first_seen AS (SELECT user_key, MIN(event_date) AS first_date FROM all_users GROUP BY user_key) SELECT to_char(date_trunc('week', a.event_date::date), 'YYYY-MM-DD') AS date, a.template AS template, COUNT(DISTINCT a.user_key) AS users FROM all_users a JOIN first_seen f ON f.user_key = a.user_key WHERE a.event_date <> f.first_date AND a.template <> 'unknown' AND (a.event_date <= to_char(CURRENT_DATE, 'YYYY-MM-DD') AND ('{{timeRange}}' IN ('', 'all') OR ('{{timeRange}}' = '7d' AND a.event_date >= to_char(CURRENT_DATE - INTERVAL '7 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '30d' AND a.event_date >= to_char(CURRENT_DATE - INTERVAL '30 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '90d' AND a.event_date >= to_char(CURRENT_DATE - INTERVAL '90 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '180d' AND a.event_date >= to_char(CURRENT_DATE - INTERVAL '180 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '365d' AND a.event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD')))) GROUP BY 1, 2 ORDER BY date, template",
504
504
  "config": {
505
505
  "xKey": "date",
506
506
  "yKey": "users",
@@ -511,7 +511,7 @@
511
511
  "valueKey": "users"
512
512
  },
513
513
  "stacked": true,
514
- "description": "Weekly distinct signed-in visitors who are NOT on their all-time first active day (Recurring only), stacked by inferred template/app used that week. Weeks start Monday; docs traffic and unknown template are excluded."
514
+ "description": "Weekly distinct signed-in visitors who are not on their first active day observed in the previous 365 days, stacked by inferred template/app used that week. Weeks start Monday; docs traffic and unknown template are excluded."
515
515
  }
516
516
  },
517
517
  {
@@ -51,7 +51,6 @@ export async function runDashboardReportsOnce(): Promise<{
51
51
  () =>
52
52
  sendDashboardReportSubscription(sub, {
53
53
  skipEmailWithoutScreenshot: retryAt !== null,
54
- allowLimitedFallback: retryAt === null,
55
54
  }),
56
55
  );
57
56
  if (!result.screenshotAttached) {
@@ -79,13 +78,7 @@ export async function runDashboardReportsOnce(): Promise<{
79
78
  await markDashboardReportResult(sub, "error", message);
80
79
  continue;
81
80
  }
82
- if (result.screenshotMode !== "full" && result.screenshotError) {
83
- const detail = `sent with ${result.screenshotMode} screenshot; earlier attempts failed: ${result.screenshotError}`;
84
- console.warn(`[dashboard-report] Subscription ${sub.id} ${detail}`);
85
- await markDashboardReportResult(sub, "success", detail);
86
- } else {
87
- await markDashboardReportResult(sub, "success");
88
- }
81
+ await markDashboardReportResult(sub, "success");
89
82
  } catch (err: any) {
90
83
  failed++;
91
84
  const message = err?.message ?? String(err);
@@ -6,6 +6,10 @@ import { and, asc, eq, isNull, lte, or, sql } from "drizzle-orm";
6
6
  import { getDb, schema } from "../db/index.js";
7
7
  import { loadDashboardSeed } from "./dashboard-seeds";
8
8
  import { getDashboard } from "./dashboards-store";
9
+ import {
10
+ FIRST_PARTY_DASHBOARD_ID,
11
+ repairFirstPartyRecurringUserPanels,
12
+ } from "./first-party-metric-catalog";
9
13
 
10
14
  export interface ReportSubscriptionInput {
11
15
  id?: string;
@@ -43,6 +47,8 @@ export interface AccessCtx {
43
47
  orgId: string | null;
44
48
  }
45
49
 
50
+ export const MAX_DASHBOARD_REPORT_RECIPIENTS = 5;
51
+
46
52
  export interface ReportDashboard {
47
53
  id: string;
48
54
  title: string;
@@ -55,10 +61,14 @@ export async function getReportDashboard(
55
61
  ): Promise<ReportDashboard | null> {
56
62
  const dashboard = await getDashboard(dashboardId, ctx);
57
63
  if (dashboard?.kind === "sql") {
64
+ const config =
65
+ dashboardId === FIRST_PARTY_DASHBOARD_ID
66
+ ? repairFirstPartyRecurringUserPanels(dashboard.config).config
67
+ : dashboard.config;
58
68
  return {
59
69
  id: dashboard.id,
60
70
  title: dashboard.title,
61
- config: dashboard.config,
71
+ config,
62
72
  };
63
73
  }
64
74
 
@@ -78,6 +88,19 @@ function nowIso(): string {
78
88
  return new Date().toISOString();
79
89
  }
80
90
 
91
+ const DASHBOARD_REPORT_ERROR_MAX_LENGTH = 2_000;
92
+ const DASHBOARD_REPORT_ERROR_OMISSION = "\n… [truncated] …\n";
93
+
94
+ export function truncateDashboardReportError(error: string): string {
95
+ if (error.length <= DASHBOARD_REPORT_ERROR_MAX_LENGTH) return error;
96
+
97
+ const retainedLength =
98
+ DASHBOARD_REPORT_ERROR_MAX_LENGTH - DASHBOARD_REPORT_ERROR_OMISSION.length;
99
+ const prefixLength = Math.ceil(retainedLength / 2);
100
+ const suffixLength = Math.floor(retainedLength / 2);
101
+ return `${error.slice(0, prefixLength)}${DASHBOARD_REPORT_ERROR_OMISSION}${error.slice(-suffixLength)}`;
102
+ }
103
+
81
104
  function safeJsonParse<T>(raw: unknown, fallback: T): T {
82
105
  if (typeof raw !== "string") return fallback;
83
106
  try {
@@ -88,7 +111,9 @@ function safeJsonParse<T>(raw: unknown, fallback: T): T {
88
111
  }
89
112
  }
90
113
 
91
- function normalizeRecipients(recipients: string[]): string[] {
114
+ export function normalizeDashboardReportRecipients(
115
+ recipients: string[],
116
+ ): string[] {
92
117
  const seen = new Set<string>();
93
118
  const normalized: string[] = [];
94
119
  for (const raw of recipients) {
@@ -97,6 +122,14 @@ function normalizeRecipients(recipients: string[]): string[] {
97
122
  seen.add(email);
98
123
  normalized.push(email);
99
124
  }
125
+ if (normalized.length === 0) {
126
+ throw new Error("At least one recipient is required");
127
+ }
128
+ if (normalized.length > MAX_DASHBOARD_REPORT_RECIPIENTS) {
129
+ throw new Error(
130
+ `Dashboard reports support at most ${MAX_DASHBOARD_REPORT_RECIPIENTS} recipients; use a mailing-list address for larger audiences`,
131
+ );
132
+ }
100
133
  return normalized;
101
134
  }
102
135
 
@@ -239,6 +272,8 @@ export function lastDailyRunAt(
239
272
  }
240
273
 
241
274
  const DASHBOARD_REPORT_RETRY_WINDOW_MS = 60 * 60 * 1000;
275
+ // This is the earliest nextRunAt. The generated */15 cron means the actual
276
+ // retry occurs on the first sweep after this floor, not exactly ten minutes later.
242
277
  const DASHBOARD_REPORT_RETRY_DELAY_MS = 10 * 60 * 1000;
243
278
 
244
279
  export function dashboardReportRetryAt(
@@ -348,10 +383,7 @@ export async function saveDashboardReportSubscription(
348
383
  throw Object.assign(new Error("Dashboard not found"), { statusCode: 404 });
349
384
  }
350
385
 
351
- const recipients = normalizeRecipients(input.recipients);
352
- if (recipients.length === 0) {
353
- throw new Error("At least one recipient is required");
354
- }
386
+ const recipients = normalizeDashboardReportRecipients(input.recipients);
355
387
 
356
388
  const timeOfDay = assertTimeOfDay(input.timeOfDay);
357
389
  const timezone = assertTimezone(input.timezone);
@@ -539,7 +571,7 @@ export async function markDashboardReportResult(
539
571
  .update(schema.dashboardReportSubscriptions)
540
572
  .set({
541
573
  lastStatus: status,
542
- lastError: error ? error.slice(0, 500) : null,
574
+ lastError: error ? truncateDashboardReportError(error) : null,
543
575
  nextRunAt: sub.enabled
544
576
  ? (options?.nextRunAt ??
545
577
  nextDailyRunAt(sub.timeOfDay, sub.timezone, new Date()))