@agent-native/core-corpus 0.210.2 → 0.211.0

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 (192) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/actions/references/action-fields.md +5 -3
  3. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +41 -2
  4. package/corpus/templates/analytics/.agents/skills/data-querying/SKILL.md +4 -2
  5. package/corpus/templates/analytics/CHANGELOG.md +18 -0
  6. package/corpus/templates/analytics/actions/build-data-index.ts +11 -2
  7. package/corpus/templates/analytics/actions/configure-content-collection.ts +29 -4
  8. package/corpus/templates/analytics/actions/configure-first-party-events-model.ts +9 -7
  9. package/corpus/templates/analytics/actions/create-analytics-public-key.ts +4 -0
  10. package/corpus/templates/analytics/actions/estimate-content-signups-cost.ts +72 -0
  11. package/corpus/templates/analytics/actions/estimate-query-cost.ts +16 -134
  12. package/corpus/templates/analytics/actions/list-sigma-workbooks.ts +123 -42
  13. package/corpus/templates/analytics/actions/migrate-sigma-workbook.ts +20 -17
  14. package/corpus/templates/analytics/actions/revoke-analytics-public-key.ts +4 -0
  15. package/corpus/templates/analytics/app/components/dashboard/SigmaMigrationDialog.tsx +246 -680
  16. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +1 -1
  17. package/corpus/templates/analytics/app/i18n/en-US.ts +54 -55
  18. package/corpus/templates/analytics/app/pages/DashboardOverview.tsx +0 -1
  19. package/corpus/templates/analytics/app/pages/DataSources.tsx +56 -0
  20. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/filter-vars.ts +4 -3
  21. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +26 -8
  22. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/interpolate.ts +3 -1
  23. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/panel-cost-notice.ts +172 -0
  24. package/corpus/templates/analytics/app/pages/data-sources/SearchConsoleDetail.tsx +441 -0
  25. package/corpus/templates/analytics/changelog/2026-10-15-a-content-dashboard-lists-every-blog-article-with-its-author.md +6 -0
  26. package/corpus/templates/analytics/changelog/2026-10-15-data-sources-has-a-google-search-console-card-that-shows-the.md +6 -0
  27. package/corpus/templates/analytics/changelog/2026-10-16-the-content-dashboard-can-show-signups-per-blog-article.md +6 -0
  28. package/corpus/templates/analytics/changelog/2026-10-19-creating-or-revoking-a-public-analytics-key-now-asks-you-to-.md +6 -0
  29. package/corpus/templates/analytics/changelog/2026-10-19-search-sigma-workbooks-by-name-and-open-ones-a-teammate-alre.md +6 -0
  30. package/corpus/templates/analytics/changelog/2026-10-20-blog-signup-attribution-excludes-signups-before-the-first-vi.md +6 -0
  31. package/corpus/templates/analytics/changelog/2026-10-20-collapsed-analytics-navigation-starts-at-the-top.md +6 -0
  32. package/corpus/templates/analytics/changelog/2026-10-20-content-keeps-successful-coverage-dates-and-missing-day-gaps.md +6 -0
  33. package/corpus/templates/analytics/changelog/2026-10-20-private-github-repositories-can-be-indexed-with-your-connect.md +6 -0
  34. package/corpus/templates/analytics/changelog/2026-10-20-search-console-preserves-backfill-and-paused-collection.md +6 -0
  35. package/corpus/templates/analytics/changelog/2026-10-20-search-console-refreshes-historical-coverage-when-properties.md +6 -0
  36. package/corpus/templates/analytics/changelog/2026-10-20-search-console-setup-keeps-saved-properties-readable-and-sup.md +6 -0
  37. package/corpus/templates/analytics/seeds/dashboards/content-view.json +182 -0
  38. package/corpus/templates/analytics/server/db/schema.ts +1 -0
  39. package/corpus/templates/analytics/server/lib/bi-migration/external-dashboards-store.ts +56 -18
  40. package/corpus/templates/analytics/server/lib/bi-migration/unfinished-import.ts +64 -0
  41. package/corpus/templates/analytics/server/lib/builtin-dashboard-pass.ts +17 -2
  42. package/corpus/templates/analytics/server/lib/content-metrics/collect.ts +2 -0
  43. package/corpus/templates/analytics/server/lib/content-metrics/config.ts +92 -3
  44. package/corpus/templates/analytics/server/lib/content-metrics/legacy-content-view.json +172 -0
  45. package/corpus/templates/analytics/server/lib/content-metrics/migration.ts +2 -0
  46. package/corpus/templates/analytics/server/lib/content-metrics/steps/daily-metrics.ts +8 -3
  47. package/corpus/templates/analytics/server/lib/content-metrics/steps/search-console.ts +16 -3
  48. package/corpus/templates/analytics/server/lib/content-metrics/steps/signups.ts +240 -0
  49. package/corpus/templates/analytics/server/lib/content-metrics/store.ts +5 -0
  50. package/corpus/templates/analytics/server/lib/dashboard-catalog.ts +21 -0
  51. package/corpus/templates/analytics/server/lib/dashboard-panel-verification.ts +300 -5
  52. package/corpus/templates/analytics/server/lib/dashboard-seeds.ts +2 -0
  53. package/corpus/templates/analytics/server/lib/dashboards-store.ts +28 -0
  54. package/corpus/templates/analytics/server/lib/first-party-analytics-backend.ts +128 -24
  55. package/corpus/templates/analytics/server/lib/google-analytics.ts +56 -16
  56. package/corpus/templates/analytics/server/lib/index-build-github.ts +5 -0
  57. package/corpus/templates/analytics/server/lib/provider-credentials.ts +47 -2
  58. package/corpus/templates/analytics/server/lib/query-cost-estimate.ts +184 -5
  59. package/corpus/templates/analytics/server/plugins/db.ts +9 -1
  60. package/corpus/templates/analytics/shared/builtin-dashboard-ids.ts +2 -0
  61. package/corpus/templates/analytics/shared/search-console-property.ts +78 -0
  62. package/corpus/templates/assets/.agents/skills/actions/references/action-fields.md +5 -3
  63. package/corpus/templates/brain/.agents/skills/actions/references/action-fields.md +5 -3
  64. package/corpus/templates/calendar/.agents/skills/actions/references/action-fields.md +5 -3
  65. package/corpus/templates/calendar/actions/delete-event.ts +1 -3
  66. package/corpus/templates/calendar/actions/delete-events.ts +1 -2
  67. package/corpus/templates/calendar/actions/event-action-helpers.ts +0 -4
  68. package/corpus/templates/calendar/actions/update-event.ts +2 -6
  69. package/corpus/templates/chat/.agents/skills/actions/references/action-fields.md +5 -3
  70. package/corpus/templates/clips/.agents/skills/actions/references/action-fields.md +5 -3
  71. package/corpus/templates/clips/CHANGELOG.md +11 -24
  72. package/corpus/templates/clips/actions/create-recording-upload.ts +5 -1
  73. package/corpus/templates/clips/app/components/player/chapter-list.tsx +7 -6
  74. package/corpus/templates/clips/app/components/player/comment-composer.tsx +5 -1
  75. package/corpus/templates/clips/app/components/player/player-controls.tsx +1 -0
  76. package/corpus/templates/clips/app/components/player/scrubber.tsx +4 -2
  77. package/corpus/templates/clips/app/i18n/en-US.ts +3 -3
  78. package/corpus/templates/clips/app/lib/extension-auth.ts +70 -0
  79. package/corpus/templates/clips/app/root.tsx +22 -10
  80. package/corpus/templates/clips/app/routes/_app.notifications.tsx +3 -0
  81. package/corpus/templates/clips/app/routes/import.tsx +115 -110
  82. package/corpus/templates/clips/changelog/2026-10-16-agent-uploads-can-use-free-clips-storage.md +6 -0
  83. package/corpus/templates/clips/changelog/2026-10-19-agents-see-a-clip-s-frames-as-soon-as-it-finishes-processing.md +6 -0
  84. package/corpus/templates/clips/changelog/2026-10-19-chapter-list-times-now-match-the-player-s-clock-and-each-cha.md +6 -0
  85. package/corpus/templates/clips/changelog/2026-10-19-editing-a-comment-puts-the-cursor-after-its-text-and-reply-o.md +6 -0
  86. package/corpus/templates/clips/changelog/2026-10-19-youtube-import-followups.md +6 -0
  87. package/corpus/templates/clips/changelog/2026-10-20-locally-saved-desktop-recordings-now-appear-in-the-recovery-.md +6 -0
  88. package/corpus/templates/clips/desktop/src/app.tsx +29 -186
  89. package/corpus/templates/clips/desktop/src/components/RecordingRecovery.tsx +171 -16
  90. package/corpus/templates/clips/desktop/src/lib/record-first.ts +9 -32
  91. package/corpus/templates/clips/desktop/src/lib/recording-recovery.ts +30 -1
  92. package/corpus/templates/clips/server/lib/agent-frame-request.ts +5 -2
  93. package/corpus/templates/clips/server/lib/recording-upload-ticket.ts +8 -0
  94. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +2 -0
  95. package/corpus/templates/content/.agents/skills/actions/references/action-fields.md +5 -3
  96. package/corpus/templates/content/CHANGELOG.md +12 -0
  97. package/corpus/templates/content/actions/_content-database-personal-view.ts +5 -15
  98. package/corpus/templates/content/actions/_database-navigation.ts +3 -3
  99. package/corpus/templates/content/actions/_database-utils.ts +10 -22
  100. package/corpus/templates/content/actions/_database-view-setup.ts +16 -76
  101. package/corpus/templates/content/actions/create-comment-ai-suggestion.ts +93 -44
  102. package/corpus/templates/content/actions/export-document.ts +2 -14
  103. package/corpus/templates/content/actions/suggest-document-edit.ts +5 -7
  104. package/corpus/templates/content/actions/update-content-database-view.ts +2 -14
  105. package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +86 -30
  106. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +64 -28
  107. package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +6 -46
  108. package/corpus/templates/content/app/components/editor/PageDraftRecovery.tsx +72 -7
  109. package/corpus/templates/content/app/components/editor/TableHoverControls.tsx +292 -103
  110. package/corpus/templates/content/app/components/editor/ThreadSuggestion.tsx +147 -40
  111. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +108 -11
  112. package/corpus/templates/content/app/components/editor/database/ContentTable.tsx +2 -1
  113. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +150 -166
  114. package/corpus/templates/content/app/components/editor/database/database-cell-comments.tsx +515 -0
  115. package/corpus/templates/content/app/components/editor/database/database-item-comments.tsx +5 -43
  116. package/corpus/templates/content/app/components/editor/database/filter-sort.ts +8 -783
  117. package/corpus/templates/content/app/components/editor/live-editor-session.ts +5 -1
  118. package/corpus/templates/content/app/components/editor/page-draft-journal.ts +61 -1
  119. package/corpus/templates/content/app/components/editor/property-comments.tsx +35 -4
  120. package/corpus/templates/content/app/components/editor/property-date-format.ts +45 -0
  121. package/corpus/templates/content/app/components/editor/thread-suggestions.ts +30 -0
  122. package/corpus/templates/content/app/components/editor/useDocumentReconcileRecovery.ts +23 -4
  123. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +36 -10
  124. package/corpus/templates/content/app/global.css +23 -0
  125. package/corpus/templates/content/app/i18n-data.ts +1 -0
  126. package/corpus/templates/content/changelog/2026-10-16-opening-a-page-in-a-new-tab-while-another-tab-is-still-savin.md +6 -0
  127. package/corpus/templates/content/changelog/2026-10-16-typing-steadily-in-one-tab-no-longer-makes-another-open-tab-.md +6 -0
  128. package/corpus/templates/content/changelog/2026-10-19-database-views-can-filter-the-same-property-more-than-once-n.md +6 -0
  129. package/corpus/templates/content/changelog/2026-10-19-edits-made-from-a-block-s-menu-or-a-table-s-row-and-column-m.md +6 -0
  130. package/corpus/templates/content/changelog/2026-10-19-starting-a-comment-puts-the-cursor-in-the-comment-box-ready-.md +6 -0
  131. package/corpus/templates/content/changelog/2026-10-19-table-row-and-column-options-now-appear-for-the-row-or-colum.md +6 -0
  132. package/corpus/templates/content/docs/product/capabilities/content.comment.page-owned.md +5 -1
  133. package/corpus/templates/content/docs/product/capabilities/content.revision.suggestions.md +3 -1
  134. package/corpus/templates/content/e2e/helpers.ts +10 -0
  135. package/corpus/templates/content/shared/api.ts +18 -12
  136. package/corpus/templates/content/shared/database-query.ts +107 -13
  137. package/corpus/templates/content/shared/suggestion-diff.ts +35 -0
  138. package/corpus/templates/crm/.agents/skills/actions/references/action-fields.md +5 -3
  139. package/corpus/templates/design/.agents/skills/actions/references/action-fields.md +5 -3
  140. package/corpus/templates/design/CHANGELOG.md +2 -0
  141. package/corpus/templates/design/actions/delete-file.ts +1 -4
  142. package/corpus/templates/design/app/components/design/LayersPanel.tsx +10 -1
  143. package/corpus/templates/design/app/components/visual-editor/ReviewCanvasPins.tsx +24 -4
  144. package/corpus/templates/design/app/pages/design-editor/commands/mode-change.ts +14 -9
  145. package/corpus/templates/design/app/pages/design-editor/domains/use-editor-modes.ts +23 -2
  146. package/corpus/templates/design/app/pages/design-editor/tool-state.ts +29 -0
  147. package/corpus/templates/design/changelog/2026-10-19-reply-and-edit-on-a-comment-in-the-sidebar-or-on-a-canvas-pi.md +6 -0
  148. package/corpus/templates/design/changelog/2026-10-20-comments-return-to-the-mode-they-came-from-and-layers-search.md +6 -0
  149. package/corpus/templates/design/e2e/global-setup.ts +185 -85
  150. package/corpus/templates/dispatch/.agents/skills/actions/references/action-fields.md +5 -3
  151. package/corpus/templates/dispatch/CHANGELOG.md +4 -0
  152. package/corpus/templates/dispatch/actions/apply-workspace-connection-setup.ts +4 -0
  153. package/corpus/templates/dispatch/actions/delete-workspace-connection.ts +4 -0
  154. package/corpus/templates/dispatch/actions/set-workspace-connection-grant.ts +4 -0
  155. package/corpus/templates/dispatch/actions/upsert-workspace-connection.ts +4 -0
  156. package/corpus/templates/dispatch/changelog/2026-10-19-vault-secrets-grants-link-tokens-app-access-and-workspace-co.md +6 -0
  157. package/corpus/templates/factory/.agents/skills/actions/references/action-fields.md +5 -3
  158. package/corpus/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +105 -33
  159. package/corpus/templates/factory/.agents/skills/review-latest-feedback/references/channel-inventory.md +128 -74
  160. package/corpus/templates/factory/.agents/skills/review-latest-feedback/references/slack-reaction-replies.md +39 -11
  161. package/corpus/templates/forms/.agents/skills/actions/references/action-fields.md +5 -3
  162. package/corpus/templates/mail/.agents/skills/actions/references/action-fields.md +5 -3
  163. package/corpus/templates/mail/.agents/skills/contacts-and-crm/SKILL.md +22 -17
  164. package/corpus/templates/mail/AGENTS.md +1 -1
  165. package/corpus/templates/mail/CHANGELOG.md +14 -0
  166. package/corpus/templates/mail/actions/provider-api-request.ts +3 -3
  167. package/corpus/templates/mail/app/components/email/IntegrationsSidebar.tsx +211 -54
  168. package/corpus/templates/mail/app/hooks/use-inbox-threads.ts +26 -2
  169. package/corpus/templates/mail/app/i18n/en-US.ts +5 -1
  170. package/corpus/templates/mail/app/lib/gong-connection.ts +27 -0
  171. package/corpus/templates/mail/app/lib/thread-cache.ts +110 -9
  172. package/corpus/templates/mail/app/root.tsx +10 -2
  173. package/corpus/templates/mail/changelog/2026-10-20-connected-gong-settings-now-open-the-shared-integration-mana.md +6 -0
  174. package/corpus/templates/mail/changelog/2026-10-20-gong-connection-details-now-match-the-call-data-available-in.md +6 -0
  175. package/corpus/templates/mail/changelog/2026-10-20-improve-gong-setup-with-a-click-to-connect-flow-and-permissi.md +6 -0
  176. package/corpus/templates/mail/changelog/2026-10-20-mail-lets-you-retry-when-it-cannot-check-your-gong-access.md +6 -0
  177. package/corpus/templates/mail/changelog/2026-10-20-mail-thread-cache-stays-isolated-when-mailbox-scope-changes.md +6 -0
  178. package/corpus/templates/mail/changelog/2026-10-20-the-inbox-and-recently-opened-threads-now-open-instantly-fro.md +6 -0
  179. package/corpus/templates/mail/server/handlers/emails.ts +9 -0
  180. package/corpus/templates/plan/.agents/skills/actions/references/action-fields.md +5 -3
  181. package/corpus/templates/plan/CHANGELOG.md +4 -0
  182. package/corpus/templates/plan/app/pages/PlansPage.tsx +173 -33
  183. package/corpus/templates/plan/changelog/2026-10-19-opening-a-comment-thread-puts-the-cursor-in-the-reply-box-an.md +6 -0
  184. package/corpus/templates/slides/.agents/skills/actions/references/action-fields.md +5 -3
  185. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +6 -42
  186. package/corpus/templates/slides/app/components/editor/FloatingToolbar.tsx +3 -7
  187. package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +5 -14
  188. package/corpus/templates/slides/app/i18n/en-US.ts +0 -2
  189. package/corpus/templates/slides/app/pages/DeckEditor.tsx +27 -33
  190. package/corpus/templates/slides/scripts/edit-fidelity/run.ts +1 -3
  191. package/corpus/templates/tasks/.agents/skills/actions/references/action-fields.md +5 -3
  192. package/package.json +1 -1
@@ -0,0 +1,182 @@
1
+ {
2
+ "name": "Content",
3
+ "description": "Blog articles with authors, daily GA4 pageviews, Search Console clicks and impressions, and first-touch signups, from Analytics' daily content collection. Reads only Analytics' own tables, so opening it calls no provider.",
4
+ "filters": [
5
+ {
6
+ "id": "timeRange",
7
+ "type": "select",
8
+ "label": "Time range",
9
+ "default": "28d",
10
+ "options": [
11
+ {
12
+ "value": "7d",
13
+ "label": "Last 7 days"
14
+ },
15
+ {
16
+ "value": "28d",
17
+ "label": "Last 28 days"
18
+ },
19
+ {
20
+ "value": "90d",
21
+ "label": "Last 90 days"
22
+ },
23
+ {
24
+ "value": "all",
25
+ "label": "All time"
26
+ }
27
+ ]
28
+ }
29
+ ],
30
+ "panels": [
31
+ {
32
+ "id": "collection-status",
33
+ "title": "Collection",
34
+ "chartType": "table",
35
+ "source": "first-party",
36
+ "width": 3,
37
+ "sql": "WITH latest AS ( SELECT step, status, started_at, window_end, error_message FROM ( SELECT step, status, started_at, window_end, error_message, ROW_NUMBER() OVER (PARTITION BY step ORDER BY started_at DESC) AS rn FROM analytics_content_collection_runs ) ranked WHERE rn = 1 ) SELECT CASE step WHEN 'builder_blog' THEN 'Blog articles' WHEN 'ga4' THEN 'GA4 pageviews' WHEN 'search_console' THEN 'Search Console' WHEN 'signups' THEN 'Signups' ELSE step END AS source, CASE status WHEN 'succeeded' THEN 'Collected' WHEN 'failed' THEN 'Failed' WHEN 'cost_limited' THEN 'Cost-limited' WHEN 'not_configured' THEN 'Not set up' WHEN 'running' THEN 'Running' ELSE status END AS status, started_at AS last_run, (SELECT window_end FROM analytics_content_collection_runs successful WHERE successful.step = latest.step AND successful.status = 'succeeded' ORDER BY successful.started_at DESC LIMIT 1) AS data_through, error_message AS detail FROM latest ORDER BY CASE step WHEN 'builder_blog' THEN 1 WHEN 'ga4' THEN 2 WHEN 'search_console' THEN 3 WHEN 'signups' THEN 4 ELSE 5 END",
38
+ "config": {
39
+ "timeScope": "all-time",
40
+ "description": "Latest run across all time for each source. Data through is the last successful coverage boundary.",
41
+ "columns": [
42
+ {
43
+ "key": "source",
44
+ "label": "Source"
45
+ },
46
+ {
47
+ "key": "status",
48
+ "label": "Status"
49
+ },
50
+ {
51
+ "key": "last_run",
52
+ "label": "Last run",
53
+ "format": "date"
54
+ },
55
+ {
56
+ "key": "data_through",
57
+ "label": "Data through"
58
+ },
59
+ {
60
+ "key": "detail",
61
+ "label": "Detail"
62
+ }
63
+ ]
64
+ }
65
+ },
66
+ {
67
+ "id": "articles",
68
+ "title": "Articles",
69
+ "chartType": "table",
70
+ "source": "first-party",
71
+ "width": 3,
72
+ "sql": "WITH latest AS ( SELECT step, status, started_at, window_end, error_message FROM ( SELECT step, status, started_at, window_end, error_message, ROW_NUMBER() OVER (PARTITION BY step ORDER BY started_at DESC) AS rn FROM analytics_content_collection_runs ) ranked WHERE rn = 1 ), totals AS ( SELECT item_id, SUM(value) FILTER (WHERE metric = 'ga4_pageviews') AS pageviews, COUNT(*) FILTER (WHERE metric = 'ga4_pageviews') AS pageview_rows, SUM(value) FILTER (WHERE metric = 'search_console_clicks') AS clicks, SUM(value) FILTER (WHERE metric = 'search_console_impressions') AS impressions, COUNT(*) FILTER (WHERE metric = 'search_console_clicks') AS search_rows, SUM(value) FILTER (WHERE metric = 'signups_first_touch') AS signups, SUM(value) FILTER (WHERE metric = 'signups_first_touch_source') AS source_signups, COUNT(*) FILTER (WHERE metric = 'signups_first_touch') AS signup_rows FROM analytics_content_metric_snapshots WHERE (metric_date <= to_char(CURRENT_DATE, 'YYYY-MM-DD') AND ('{{timeRange}}' IN ('', 'all') OR ('{{timeRange}}' = '7d' AND metric_date >= to_char(CURRENT_DATE - INTERVAL '7 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '28d' AND metric_date >= to_char(CURRENT_DATE - INTERVAL '28 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '90d' AND metric_date >= to_char(CURRENT_DATE - INTERVAL '90 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = 'custom' AND metric_date >= '{{timeRangeStart}}' AND metric_date <= '{{timeRangeEnd}}'))) GROUP BY item_id ) SELECT title, href, author, published, pageviews, clicks, impressions, signups, source_signups, NULLIF(COALESCE(ga4_missing, '') || CASE WHEN ga4_missing IS NOT NULL AND search_missing IS NOT NULL THEN ' · ' ELSE '' END || COALESCE(search_missing, '') || CASE WHEN signups_missing IS NOT NULL AND (ga4_missing IS NOT NULL OR search_missing IS NOT NULL) THEN ' · ' ELSE '' END || COALESCE(signups_missing, ''), '') AS not_collected FROM ( SELECT i.title, i.url AS href, i.author, i.published_at, substr(i.published_at, 1, 10) AS published, t.pageviews, t.clicks, t.impressions, t.signups, t.source_signups, CASE WHEN COALESCE(t.pageview_rows, 0) > 0 THEN NULL ELSE 'GA4: ' || CASE ga4_run.status WHEN 'failed' THEN 'failed' WHEN 'cost_limited' THEN 'cost-limited' WHEN 'not_configured' THEN 'not set up' WHEN 'running' THEN 'running' WHEN 'succeeded' THEN 'not reported in this window' ELSE 'not collected yet' END END AS ga4_missing, CASE WHEN COALESCE(t.search_rows, 0) > 0 THEN NULL ELSE 'Search Console: ' || CASE search_run.status WHEN 'failed' THEN 'failed' WHEN 'cost_limited' THEN 'cost-limited' WHEN 'not_configured' THEN 'not set up' WHEN 'running' THEN 'running' WHEN 'succeeded' THEN 'not reported in this window' ELSE 'not collected yet' END END AS search_missing, CASE WHEN COALESCE(t.signup_rows, 0) > 0 THEN NULL ELSE 'Signups: ' || CASE signups_run.status WHEN 'failed' THEN 'failed' WHEN 'cost_limited' THEN 'cost-limited' WHEN 'not_configured' THEN 'not set up' WHEN 'running' THEN 'running' WHEN 'succeeded' THEN 'not reported in this window' ELSE 'not collected yet' END END AS signups_missing FROM analytics_content_items i LEFT JOIN totals t ON t.item_id = i.id LEFT JOIN latest ga4_run ON ga4_run.step = 'ga4' LEFT JOIN latest search_run ON search_run.step = 'search_console' LEFT JOIN latest signups_run ON signups_run.step = 'signups' WHERE i.source = 'builder_blog' ) articles ORDER BY published_at DESC NULLS LAST LIMIT 1000",
73
+ "config": {
74
+ "timeScope": "dashboard",
75
+ "description": "The 1,000 most recently published collected blog articles. A blank number means that source has no data for the article in this window; Not collected says why.",
76
+ "sortable": true,
77
+ "columns": [
78
+ {
79
+ "key": "title",
80
+ "label": "Article",
81
+ "format": "link",
82
+ "linkKey": "href"
83
+ },
84
+ {
85
+ "key": "href",
86
+ "hidden": true
87
+ },
88
+ {
89
+ "key": "author",
90
+ "label": "Author"
91
+ },
92
+ {
93
+ "key": "published",
94
+ "label": "Published"
95
+ },
96
+ {
97
+ "key": "pageviews",
98
+ "label": "Pageviews",
99
+ "format": "number"
100
+ },
101
+ {
102
+ "key": "clicks",
103
+ "label": "Search clicks",
104
+ "format": "number"
105
+ },
106
+ {
107
+ "key": "impressions",
108
+ "label": "Impressions",
109
+ "format": "number"
110
+ },
111
+ {
112
+ "key": "signups",
113
+ "label": "Signups",
114
+ "format": "number"
115
+ },
116
+ {
117
+ "key": "source_signups",
118
+ "label": "Source signups",
119
+ "format": "number"
120
+ },
121
+ {
122
+ "key": "not_collected",
123
+ "label": "Not collected"
124
+ }
125
+ ]
126
+ }
127
+ },
128
+ {
129
+ "id": "daily-trend",
130
+ "title": "Daily pageviews and search clicks",
131
+ "chartType": "line",
132
+ "source": "first-party",
133
+ "width": 3,
134
+ "sql": "WITH RECURSIVE dates AS (SELECT CASE '{{timeRange}}' WHEN '7d' THEN CURRENT_DATE - 7 WHEN '28d' THEN CURRENT_DATE - 28 WHEN '90d' THEN CURRENT_DATE - 90 WHEN 'custom' THEN NULLIF('{{timeRangeStart}}', '')::date ELSE (SELECT MIN(metric_date)::date FROM analytics_content_metric_snapshots) END AS day UNION ALL SELECT day + 1 FROM dates WHERE day < CASE WHEN '{{timeRange}}' = 'custom' THEN LEAST(CURRENT_DATE, NULLIF('{{timeRangeEnd}}', '')::date) ELSE CURRENT_DATE END), totals AS (SELECT s.metric_date AS date, SUM(s.value) FILTER (WHERE s.metric = 'ga4_pageviews') AS pageviews, SUM(s.value) FILTER (WHERE s.metric = 'search_console_clicks') AS search_clicks FROM analytics_content_metric_snapshots s JOIN analytics_content_items i ON i.id = s.item_id WHERE i.source = 'builder_blog' AND s.metric IN ('ga4_pageviews', 'search_console_clicks') AND (s.metric_date <= to_char(CURRENT_DATE, 'YYYY-MM-DD') AND ('{{timeRange}}' IN ('', 'all') OR ('{{timeRange}}' = '7d' AND s.metric_date >= to_char(CURRENT_DATE - INTERVAL '7 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '28d' AND s.metric_date >= to_char(CURRENT_DATE - INTERVAL '28 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '90d' AND s.metric_date >= to_char(CURRENT_DATE - INTERVAL '90 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = 'custom' AND s.metric_date >= '{{timeRangeStart}}' AND s.metric_date <= '{{timeRangeEnd}}'))) GROUP BY s.metric_date ORDER BY s.metric_date) SELECT to_char(dates.day, 'YYYY-MM-DD') AS date, totals.pageviews, totals.search_clicks FROM dates LEFT JOIN totals ON totals.date = to_char(dates.day, 'YYYY-MM-DD') WHERE dates.day <= CASE WHEN '{{timeRange}}' = 'custom' THEN LEAST(CURRENT_DATE, NULLIF('{{timeRangeEnd}}', '')::date) ELSE CURRENT_DATE END ORDER BY dates.day",
135
+ "config": {
136
+ "timeScope": "dashboard",
137
+ "xKey": "date",
138
+ "yKeys": ["pageviews", "search_clicks"],
139
+ "seriesLabels": {
140
+ "pageviews": "Pageviews",
141
+ "search_clicks": "Search clicks"
142
+ },
143
+ "description": "All blog articles. A day a source hasn't reported is a gap, not a zero."
144
+ }
145
+ },
146
+ {
147
+ "id": "search-pages",
148
+ "title": "Other pages in search",
149
+ "chartType": "table",
150
+ "source": "first-party",
151
+ "width": 3,
152
+ "sql": "SELECT i.url AS page, i.url AS href, SUM(s.value) FILTER (WHERE s.metric = 'search_console_clicks') AS clicks, SUM(s.value) FILTER (WHERE s.metric = 'search_console_impressions') AS impressions FROM analytics_content_items i JOIN analytics_content_metric_snapshots s ON s.item_id = i.id WHERE i.source = 'search_console' AND (s.metric_date <= to_char(CURRENT_DATE, 'YYYY-MM-DD') AND ('{{timeRange}}' IN ('', 'all') OR ('{{timeRange}}' = '7d' AND s.metric_date >= to_char(CURRENT_DATE - INTERVAL '7 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '28d' AND s.metric_date >= to_char(CURRENT_DATE - INTERVAL '28 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '90d' AND s.metric_date >= to_char(CURRENT_DATE - INTERVAL '90 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = 'custom' AND s.metric_date >= '{{timeRangeStart}}' AND s.metric_date <= '{{timeRangeEnd}}'))) GROUP BY i.url ORDER BY clicks DESC NULLS LAST, impressions DESC NULLS LAST LIMIT 200",
153
+ "config": {
154
+ "timeScope": "dashboard",
155
+ "description": "Search Console pages that aren't collected blog articles.",
156
+ "sortable": true,
157
+ "columns": [
158
+ {
159
+ "key": "page",
160
+ "label": "Page",
161
+ "format": "link",
162
+ "linkKey": "href"
163
+ },
164
+ {
165
+ "key": "href",
166
+ "hidden": true
167
+ },
168
+ {
169
+ "key": "clicks",
170
+ "label": "Search clicks",
171
+ "format": "number"
172
+ },
173
+ {
174
+ "key": "impressions",
175
+ "label": "Impressions",
176
+ "format": "number"
177
+ }
178
+ ]
179
+ }
180
+ }
181
+ ]
182
+ }
@@ -1136,6 +1136,7 @@ export const analyticsContentCollectionRuns = table(
1136
1136
  status: text("status").notNull(),
1137
1137
  windowStart: text("window_start"),
1138
1138
  windowEnd: text("window_end"),
1139
+ configFingerprint: text("config_fingerprint"),
1139
1140
  startedAt: text("started_at").notNull(),
1140
1141
  finishedAt: text("finished_at"),
1141
1142
  rowCount: integer("row_count"),
@@ -107,7 +107,9 @@ const SummarySchema = z.object({
107
107
  sourceUpdatedAt: z.string().nullable(),
108
108
  status: z.enum(["not_started", "migrating", "migrated", "failed"]),
109
109
  targetDashboardId: z.string().nullable(),
110
+ migratedByEmail: z.string().nullable(),
110
111
  migratedByCurrentUser: z.boolean(),
112
+ canStartImport: z.boolean(),
111
113
  canRetryStaleClaim: z.boolean(),
112
114
  failureCode: z.enum(EXTERNAL_DASHBOARD_FAILURE_CODES).nullable(),
113
115
  providerStatus: z.number().nullable(),
@@ -251,20 +253,35 @@ function requireExternalDashboardContext(): ExternalDashboardContext {
251
253
  return { orgId, userEmail };
252
254
  }
253
255
 
254
- function toSummary(
256
+ /**
257
+ * Whether this user may claim the source now; claimExternalDashboard applies
258
+ * the same rule in SQL. Anyone who can open the workbook may start it, retry a
259
+ * failed import, or take over a stuck one. A source that already has a
260
+ * dashboard (a re-import, failed or stuck) stays with its migrator.
261
+ */
262
+ function canStartImport(
255
263
  row: ExternalDashboardRow,
256
- userEmail: string,
257
- ): ExternalDashboardSummary {
258
- const migratedByCurrentUser =
259
- row.originalMigratorEmail?.trim().toLowerCase() === userEmail;
264
+ migratedByCurrentUser: boolean,
265
+ ): boolean {
266
+ if (row.targetDashboardId !== null && !migratedByCurrentUser) return false;
267
+ if (row.status === "not_started" || row.status === "failed") return true;
260
268
  const migrationClaimedAt = row.migrationClaimedAt
261
269
  ? Date.parse(row.migrationClaimedAt)
262
270
  : Number.NaN;
263
- const canRetryStaleClaim =
271
+ return (
264
272
  row.status === "migrating" &&
265
- migratedByCurrentUser &&
266
273
  Number.isFinite(migrationClaimedAt) &&
267
- migrationClaimedAt <= Date.now() - EXTERNAL_DASHBOARD_CLAIM_STALE_AFTER_MS;
274
+ migrationClaimedAt <= Date.now() - EXTERNAL_DASHBOARD_CLAIM_STALE_AFTER_MS
275
+ );
276
+ }
277
+
278
+ function toSummary(
279
+ row: ExternalDashboardRow,
280
+ userEmail: string,
281
+ ): ExternalDashboardSummary {
282
+ const migratedByCurrentUser =
283
+ row.originalMigratorEmail?.trim().toLowerCase() === userEmail;
284
+ const startable = canStartImport(row, migratedByCurrentUser);
268
285
  return SummarySchema.parse({
269
286
  id: row.id,
270
287
  provider: row.provider,
@@ -272,9 +289,19 @@ function toSummary(
272
289
  sourceName: row.sourceName,
273
290
  sourceUpdatedAt: row.sourceUpdatedAt,
274
291
  status: row.status,
275
- targetDashboardId: migratedByCurrentUser ? row.targetDashboardId : null,
292
+ // Org members see where a migrated source landed so nobody migrates it
293
+ // twice. Opening the dashboard still goes through its own access check.
294
+ targetDashboardId:
295
+ migratedByCurrentUser || row.status === "migrated"
296
+ ? row.targetDashboardId
297
+ : null,
298
+ migratedByEmail:
299
+ row.status === "migrated" || row.status === "migrating"
300
+ ? (row.originalMigratorEmail?.trim().toLowerCase() ?? null)
301
+ : null,
276
302
  migratedByCurrentUser,
277
- canRetryStaleClaim,
303
+ canStartImport: startable,
304
+ canRetryStaleClaim: row.status === "migrating" && startable,
278
305
  failureCode: row.failureCode,
279
306
  providerStatus: row.providerStatus,
280
307
  elementDiagnostics: JSON.parse(row.elementDiagnostics),
@@ -392,14 +419,17 @@ export async function claimExternalDashboard(input: {
392
419
  const claimToken = randomUUID();
393
420
  const table = schema.externalDashboardMigrations;
394
421
  const sameMigrator = eq(table.originalMigratorEmail, userEmail);
395
- const canStart = or(
396
- eq(table.status, "not_started"),
397
- eq(table.status, "failed"),
422
+ // The SQL form of canStartImport: a source that already has a dashboard
423
+ // stays with its migrator.
424
+ const openToCaller = or(isNull(table.targetDashboardId), sameMigrator);
425
+ const canStart = and(
426
+ or(eq(table.status, "not_started"), eq(table.status, "failed")),
427
+ openToCaller,
398
428
  );
399
429
  const canRecoverStaleClaim = and(
400
430
  eq(table.status, "migrating"),
401
431
  lt(table.migrationClaimedAt, staleBefore),
402
- sameMigrator,
432
+ openToCaller,
403
433
  );
404
434
  const canRemigrate = parsed.remigrate
405
435
  ? and(eq(table.status, "migrated"), sameMigrator)
@@ -421,7 +451,6 @@ export async function claimExternalDashboard(input: {
421
451
  eq(table.orgId, orgId),
422
452
  eq(table.provider, parsed.provider),
423
453
  eq(table.sourceId, parsed.sourceId),
424
- or(isNull(table.originalMigratorEmail), sameMigrator),
425
454
  or(canStart, canRecoverStaleClaim, canRemigrate),
426
455
  ),
427
456
  )
@@ -444,16 +473,25 @@ export async function claimExternalDashboard(input: {
444
473
  .limit(1);
445
474
  if (!existing) return { claimed: false, reason: "not_found" };
446
475
 
476
+ const existingByCaller =
477
+ existing.originalMigratorEmail?.trim().toLowerCase() === userEmail;
447
478
  if (
448
- existing.originalMigratorEmail &&
449
- existing.originalMigratorEmail.toLowerCase() !== userEmail
479
+ existing.status === "failed" &&
480
+ existing.targetDashboardId !== null &&
481
+ !existingByCaller
450
482
  ) {
451
- fail("Only the original migrator may retry or remigrate this source.", {
483
+ fail("Only the person who migrated this workbook can re-import it.", {
452
484
  errorCode: "original_migrator_required",
453
485
  statusCode: 403,
454
486
  });
455
487
  }
456
488
  if (existing.status === "migrated") {
489
+ if (parsed.remigrate && !existingByCaller) {
490
+ fail("Only the person who migrated this workbook can re-import it.", {
491
+ errorCode: "original_migrator_required",
492
+ statusCode: 403,
493
+ });
494
+ }
457
495
  return { claimed: false, reason: "already_migrated" };
458
496
  }
459
497
  return { claimed: false, reason: "in_progress" };
@@ -0,0 +1,64 @@
1
+ import { isActionContractError } from "@agent-native/core/action";
2
+
3
+ import { removeDashboard } from "../dashboards-store.js";
4
+ import {
5
+ failExternalDashboard,
6
+ listExternalDashboards,
7
+ type ExternalDashboardFailureCode,
8
+ } from "./external-dashboards-store.js";
9
+
10
+ /**
11
+ * Record a Sigma import that did not finish, and delete the dashboard it wrote
12
+ * when that dashboard would otherwise be left behind. Only a first import's
13
+ * dashboard is deleted. When the claimed record already had a dashboard (a
14
+ * re-import, a recovery, or a retry of either), this run wrote over the
15
+ * migrator's own dashboard, which the mapping row still tracks.
16
+ *
17
+ * Dashboard ids include the importer's email. So when another person has
18
+ * taken over the claim, this run's dashboard is never theirs and is deleted;
19
+ * when the same person took it over, the id is shared and is kept.
20
+ */
21
+ export async function abandonSigmaImport(input: {
22
+ workbookId: string;
23
+ claimToken: string;
24
+ failureCode: ExternalDashboardFailureCode;
25
+ dashboardId: string;
26
+ wroteDashboard: boolean;
27
+ /** The claimed record's targetDashboardId, not the request's flags. */
28
+ previousDashboardId: string | null;
29
+ ctx: { email: string; orgId: string };
30
+ }): Promise<void> {
31
+ let claimLost: unknown = null;
32
+ try {
33
+ await failExternalDashboard({
34
+ provider: "sigma",
35
+ sourceId: input.workbookId,
36
+ claimToken: input.claimToken,
37
+ failureCode: input.failureCode,
38
+ preservePreviousMigration: !input.wroteDashboard,
39
+ });
40
+ } catch (error) {
41
+ if (
42
+ !isActionContractError(error) ||
43
+ error.errorCode !== "migration_claim_expired"
44
+ ) {
45
+ throw error;
46
+ }
47
+ claimLost = error;
48
+ }
49
+
50
+ if (input.wroteDashboard && input.previousDashboardId === null) {
51
+ let orphaned = true;
52
+ if (claimLost) {
53
+ const [migration] = await listExternalDashboards({
54
+ provider: "sigma",
55
+ sourceIds: [input.workbookId],
56
+ limit: 1,
57
+ });
58
+ orphaned = !migration?.migratedByCurrentUser;
59
+ }
60
+ if (orphaned) await removeDashboard(input.dashboardId, input.ctx);
61
+ }
62
+
63
+ if (claimLost) throw claimLost;
64
+ }
@@ -19,6 +19,7 @@ import {
19
19
  withoutInstallTimes,
20
20
  type BuiltinDashboardInstall,
21
21
  } from "./builtin-dashboards";
22
+ import legacyContentView from "./content-metrics/legacy-content-view.json";
22
23
  import { materializeLegacyBuiltinDashboardSettings } from "./legacy-builtin-dashboard-settings";
23
24
  import {
24
25
  nativeV2DashboardManifests,
@@ -169,6 +170,7 @@ function candidateFilter() {
169
170
  or(
170
171
  sql`${d.id} LIKE 'demo-node-exporter-%'`,
171
172
  sql`${d.id} LIKE 'native-%'`,
173
+ sql`${d.id} LIKE 'content-view-%'`,
172
174
  eq(d.id, "risk-meeting"),
173
175
  inArray(d.id, [...SEED_BUILTIN_DASHBOARD_IDS]),
174
176
  ...SEED_BUILTIN_DASHBOARD_IDS.map(
@@ -309,10 +311,23 @@ export async function runBuiltinDashboardPass(
309
311
  const built = install ? buildBuiltinDashboard(builtinId, install) : null;
310
312
  if (config && !built) reasons.push("unrecognized");
311
313
  if (config && built) {
314
+ const shippedConfigs = [built.config];
315
+ if (builtinId === "content-view") {
316
+ shippedConfigs.push({
317
+ ...legacyContentView,
318
+ catalog: {
319
+ templateId: "content-view",
320
+ templateVersion: "2026-06-08",
321
+ },
322
+ });
323
+ }
312
324
  const same =
313
325
  row.title === built.title &&
314
- stable(withoutInstallTimes(config)) ===
315
- stable(withoutInstallTimes(built.config));
326
+ shippedConfigs.some(
327
+ (shipped) =>
328
+ stable(withoutInstallTimes(config!)) ===
329
+ stable(withoutInstallTimes(normalizeDashboardConfig(shipped))),
330
+ );
316
331
  if (!same) {
317
332
  reasons.push(
318
333
  shippedVersion(config) !== shippedVersion(built.config)
@@ -9,6 +9,7 @@ import { contentRunFailureFromError, type ContentStep } from "./step.js";
9
9
  import { builderBlogStep } from "./steps/builder-blog.js";
10
10
  import { ga4Step } from "./steps/ga4.js";
11
11
  import { searchConsoleStep } from "./steps/search-console.js";
12
+ import { signupsStep } from "./steps/signups.js";
12
13
  import {
13
14
  closeStaleContentRuns,
14
15
  completeContentRun,
@@ -24,6 +25,7 @@ const STEPS: Record<ContentCollectionStepId, ContentStep> = {
24
25
  builder_blog: builderBlogStep,
25
26
  ga4: ga4Step,
26
27
  search_console: searchConsoleStep,
28
+ signups: signupsStep,
27
29
  };
28
30
 
29
31
  export type ContentStepRunSummary = {
@@ -1,5 +1,11 @@
1
1
  import { z } from "zod";
2
2
 
3
+ import {
4
+ normalizeSearchConsoleProperty,
5
+ searchConsolePropertyMessage,
6
+ searchConsolePropertyProblem,
7
+ } from "../../../shared/search-console-property.js";
8
+
3
9
  /**
4
10
  * Collection steps, in the order the daily job runs them. A step's id is the
5
11
  * `step` stored on its collection runs and the `source` on what it writes.
@@ -8,6 +14,7 @@ export const CONTENT_COLLECTION_STEPS = [
8
14
  "builder_blog",
9
15
  "ga4",
10
16
  "search_console",
17
+ "signups",
11
18
  ] as const;
12
19
  export type ContentCollectionStepId = (typeof CONTENT_COLLECTION_STEPS)[number];
13
20
 
@@ -18,20 +25,100 @@ const stepToggleSchema = z.object({ enabled: z.boolean() });
18
25
  * property such as `https://www.example.com/`. `pathPrefix` keeps a large
19
26
  * site to the pages the Content view tracks.
20
27
  */
21
- export const searchConsoleSiteSchema = z.object({
22
- siteUrl: z.string().trim().min(1),
28
+ export const storedSearchConsoleSiteSchema = z.object({
29
+ siteUrl: z.string().trim().min(1).transform(normalizeSearchConsoleProperty),
23
30
  pathPrefix: z.string().trim().startsWith("/").optional(),
24
31
  });
32
+
33
+ // Existing configs must stay readable so admins can correct or disable them.
34
+ export const searchConsoleSiteSchema = storedSearchConsoleSiteSchema.extend({
35
+ siteUrl: storedSearchConsoleSiteSchema.shape.siteUrl.superRefine(
36
+ (siteUrl, ctx) => {
37
+ const problem = searchConsolePropertyProblem(siteUrl);
38
+ if (problem) {
39
+ ctx.addIssue({
40
+ code: "custom",
41
+ message: searchConsolePropertyMessage(siteUrl, problem),
42
+ });
43
+ }
44
+ },
45
+ ),
46
+ });
25
47
  export type SearchConsoleSite = z.infer<typeof searchConsoleSiteSchema>;
26
48
 
49
+ const bigQueryTableSchema = z
50
+ .string()
51
+ .trim()
52
+ .regex(
53
+ /^[A-Za-z0-9_-]+.[A-Za-z0-9_]+.[A-Za-z0-9_]+$/,
54
+ "Use project.dataset.table",
55
+ );
56
+
57
+ const signupSourceSchema = z
58
+ .string()
59
+ .trim()
60
+ .regex(/^[a-z0-9_-]+$/);
61
+
62
+ /**
63
+ * The per-article signups join reads the warehouse, so it stays off until an
64
+ * admin names its tables and a bytes cap measured by a dry run.
65
+ */
66
+ export const signupsConfigSchema = z.object({
67
+ enabled: z.boolean(),
68
+ /** Signup events with JSON `data` holding `visitorId`, `userId` and `signupSource`. */
69
+ signupEventsTable: bigQueryTableSchema.nullable().default(null),
70
+ /** One row per visitor's first pageview: `url`, `visitor_id`, `is_bot`, `created_date`. */
71
+ firstPageviewsTable: bigQueryTableSchema.nullable().default(null),
72
+ signupSource: signupSourceSchema.nullable().default(null),
73
+ /** Earliest signup counted, as YYYY-MM-DD; it bounds the events scan. */
74
+ signupsSince: z.iso.date().nullable().default(null),
75
+ maxBytesBilled: z
76
+ .number()
77
+ .int()
78
+ .positive()
79
+ .max(750_000_000_000)
80
+ .nullable()
81
+ .default(null),
82
+ });
83
+
84
+ /**
85
+ * A change to the signups settings: only the fields given. No defaults, so
86
+ * merging it over the saved settings never resets one that was left out.
87
+ */
88
+ export const signupsSettingsUpdateSchema = z.object({
89
+ enabled: z.boolean().optional(),
90
+ signupEventsTable: bigQueryTableSchema.nullable().optional(),
91
+ firstPageviewsTable: bigQueryTableSchema.nullable().optional(),
92
+ signupSource: signupSourceSchema.nullable().optional(),
93
+ signupsSince: z.iso.date().nullable().optional(),
94
+ maxBytesBilled: z
95
+ .number()
96
+ .int()
97
+ .positive()
98
+ .max(750_000_000_000)
99
+ .nullable()
100
+ .optional(),
101
+ });
102
+ export type SignupsConfig = z.infer<typeof signupsConfigSchema>;
103
+
27
104
  export const contentCollectionConfigSchema = z.object({
28
105
  builderBlog: stepToggleSchema.default({ enabled: true }),
29
106
  ga4: stepToggleSchema.default({ enabled: true }),
30
107
  // Off until a property owner grants the service account access; a run
31
108
  // before then would only record "no access".
32
109
  searchConsole: stepToggleSchema
33
- .extend({ sites: z.array(searchConsoleSiteSchema).max(20).default([]) })
110
+ .extend({
111
+ sites: z.array(storedSearchConsoleSiteSchema).max(20).default([]),
112
+ })
34
113
  .default({ enabled: false, sites: [] }),
114
+ signups: signupsConfigSchema.default({
115
+ enabled: false,
116
+ signupEventsTable: null,
117
+ firstPageviewsTable: null,
118
+ signupSource: null,
119
+ signupsSince: null,
120
+ maxBytesBilled: null,
121
+ }),
35
122
  });
36
123
 
37
124
  export type ContentCollectionConfig = z.infer<
@@ -53,6 +140,8 @@ export function stepEnabled(
53
140
  return config.ga4.enabled;
54
141
  case "search_console":
55
142
  return config.searchConsole.enabled;
143
+ case "signups":
144
+ return config.signups.enabled;
56
145
  }
57
146
  }
58
147