@agent-native/core-corpus 0.210.2 → 0.211.0-nightly-20261020183616

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 (188) 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/.deploy-tmp/agent-native-provided-plugins.mjs +10 -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/assets/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  64. package/corpus/templates/brain/.agents/skills/actions/references/action-fields.md +5 -3
  65. package/corpus/templates/brain/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  66. package/corpus/templates/calendar/.agents/skills/actions/references/action-fields.md +5 -3
  67. package/corpus/templates/calendar/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  68. package/corpus/templates/calendar/actions/delete-event.ts +1 -3
  69. package/corpus/templates/calendar/actions/delete-events.ts +1 -2
  70. package/corpus/templates/calendar/actions/event-action-helpers.ts +0 -4
  71. package/corpus/templates/calendar/actions/update-event.ts +2 -6
  72. package/corpus/templates/chat/.agents/skills/actions/references/action-fields.md +5 -3
  73. package/corpus/templates/chat/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  74. package/corpus/templates/clips/.agents/skills/actions/references/action-fields.md +5 -3
  75. package/corpus/templates/clips/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  76. package/corpus/templates/clips/app/components/player/chapter-list.tsx +7 -6
  77. package/corpus/templates/clips/app/components/player/comment-composer.tsx +5 -1
  78. package/corpus/templates/clips/app/components/player/player-controls.tsx +1 -0
  79. package/corpus/templates/clips/app/components/player/scrubber.tsx +4 -2
  80. package/corpus/templates/clips/app/i18n/en-US.ts +3 -3
  81. package/corpus/templates/clips/app/lib/extension-auth.ts +70 -0
  82. package/corpus/templates/clips/app/root.tsx +22 -10
  83. package/corpus/templates/clips/app/routes/_app.notifications.tsx +3 -0
  84. package/corpus/templates/clips/app/routes/import.tsx +115 -110
  85. package/corpus/templates/clips/changelog/2026-10-19-agents-see-a-clip-s-frames-as-soon-as-it-finishes-processing.md +6 -0
  86. package/corpus/templates/clips/changelog/2026-10-19-chapter-list-times-now-match-the-player-s-clock-and-each-cha.md +6 -0
  87. package/corpus/templates/clips/changelog/2026-10-19-editing-a-comment-puts-the-cursor-after-its-text-and-reply-o.md +6 -0
  88. package/corpus/templates/clips/changelog/2026-10-19-youtube-import-followups.md +6 -0
  89. package/corpus/templates/clips/changelog/2026-10-20-locally-saved-desktop-recordings-now-appear-in-the-recovery-.md +6 -0
  90. package/corpus/templates/clips/desktop/src/app.tsx +29 -186
  91. package/corpus/templates/clips/desktop/src/components/RecordingRecovery.tsx +171 -16
  92. package/corpus/templates/clips/desktop/src/lib/record-first.ts +9 -32
  93. package/corpus/templates/clips/desktop/src/lib/recording-recovery.ts +30 -1
  94. package/corpus/templates/clips/server/lib/agent-frame-request.ts +5 -2
  95. package/corpus/templates/content/.agents/skills/actions/references/action-fields.md +5 -3
  96. package/corpus/templates/content/.deploy-tmp/agent-native-provided-plugins.mjs +10 -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 +82 -30
  106. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +8 -26
  107. package/corpus/templates/content/app/components/editor/PageDraftRecovery.tsx +72 -7
  108. package/corpus/templates/content/app/components/editor/TableHoverControls.tsx +292 -103
  109. package/corpus/templates/content/app/components/editor/ThreadSuggestion.tsx +147 -40
  110. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +108 -11
  111. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +122 -153
  112. package/corpus/templates/content/app/components/editor/database/filter-sort.ts +8 -783
  113. package/corpus/templates/content/app/components/editor/live-editor-session.ts +5 -1
  114. package/corpus/templates/content/app/components/editor/page-draft-journal.ts +61 -1
  115. package/corpus/templates/content/app/components/editor/thread-suggestions.ts +30 -0
  116. package/corpus/templates/content/app/components/editor/useDocumentReconcileRecovery.ts +23 -4
  117. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +36 -10
  118. package/corpus/templates/content/changelog/2026-10-16-opening-a-page-in-a-new-tab-while-another-tab-is-still-savin.md +6 -0
  119. package/corpus/templates/content/changelog/2026-10-16-typing-steadily-in-one-tab-no-longer-makes-another-open-tab-.md +6 -0
  120. package/corpus/templates/content/changelog/2026-10-19-database-views-can-filter-the-same-property-more-than-once-n.md +6 -0
  121. 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
  122. package/corpus/templates/content/changelog/2026-10-19-starting-a-comment-puts-the-cursor-in-the-comment-box-ready-.md +6 -0
  123. package/corpus/templates/content/changelog/2026-10-19-table-row-and-column-options-now-appear-for-the-row-or-colum.md +6 -0
  124. package/corpus/templates/content/docs/product/capabilities/content.revision.suggestions.md +3 -1
  125. package/corpus/templates/content/e2e/helpers.ts +10 -0
  126. package/corpus/templates/content/shared/api.ts +18 -12
  127. package/corpus/templates/content/shared/database-query.ts +107 -13
  128. package/corpus/templates/content/shared/suggestion-diff.ts +35 -0
  129. package/corpus/templates/crm/.agents/skills/actions/references/action-fields.md +5 -3
  130. package/corpus/templates/crm/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  131. package/corpus/templates/design/.agents/skills/actions/references/action-fields.md +5 -3
  132. package/corpus/templates/design/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  133. package/corpus/templates/design/.generated/bridge/native-builtin-metadata.generated.ts +301 -0
  134. package/corpus/templates/design/.generated/bridge/native-worker-source.generated.ts +6 -0
  135. package/corpus/templates/design/actions/delete-file.ts +1 -4
  136. package/corpus/templates/design/app/components/design/LayersPanel.tsx +10 -1
  137. package/corpus/templates/design/app/components/visual-editor/ReviewCanvasPins.tsx +24 -4
  138. package/corpus/templates/design/app/pages/design-editor/commands/mode-change.ts +14 -9
  139. package/corpus/templates/design/app/pages/design-editor/domains/use-editor-modes.ts +23 -2
  140. package/corpus/templates/design/app/pages/design-editor/tool-state.ts +29 -0
  141. 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
  142. package/corpus/templates/design/changelog/2026-10-20-comments-return-to-the-mode-they-came-from-and-layers-search.md +6 -0
  143. package/corpus/templates/design/e2e/global-setup.ts +185 -85
  144. package/corpus/templates/dispatch/.agents/skills/actions/references/action-fields.md +5 -3
  145. package/corpus/templates/dispatch/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  146. package/corpus/templates/dispatch/actions/apply-workspace-connection-setup.ts +4 -0
  147. package/corpus/templates/dispatch/actions/delete-workspace-connection.ts +4 -0
  148. package/corpus/templates/dispatch/actions/set-workspace-connection-grant.ts +4 -0
  149. package/corpus/templates/dispatch/actions/upsert-workspace-connection.ts +4 -0
  150. package/corpus/templates/dispatch/changelog/2026-10-19-vault-secrets-grants-link-tokens-app-access-and-workspace-co.md +6 -0
  151. package/corpus/templates/factory/.agents/skills/actions/references/action-fields.md +5 -3
  152. package/corpus/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +105 -33
  153. package/corpus/templates/factory/.agents/skills/review-latest-feedback/references/channel-inventory.md +128 -74
  154. package/corpus/templates/factory/.agents/skills/review-latest-feedback/references/slack-reaction-replies.md +39 -11
  155. package/corpus/templates/forms/.agents/skills/actions/references/action-fields.md +5 -3
  156. package/corpus/templates/forms/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  157. package/corpus/templates/mail/.agents/skills/actions/references/action-fields.md +5 -3
  158. package/corpus/templates/mail/.agents/skills/contacts-and-crm/SKILL.md +22 -17
  159. package/corpus/templates/mail/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  160. package/corpus/templates/mail/AGENTS.md +1 -1
  161. package/corpus/templates/mail/actions/provider-api-request.ts +3 -3
  162. package/corpus/templates/mail/app/components/email/IntegrationsSidebar.tsx +211 -54
  163. package/corpus/templates/mail/app/hooks/use-inbox-threads.ts +26 -2
  164. package/corpus/templates/mail/app/i18n/en-US.ts +5 -1
  165. package/corpus/templates/mail/app/lib/gong-connection.ts +27 -0
  166. package/corpus/templates/mail/app/lib/thread-cache.ts +110 -9
  167. package/corpus/templates/mail/app/root.tsx +10 -2
  168. package/corpus/templates/mail/changelog/2026-10-20-connected-gong-settings-now-open-the-shared-integration-mana.md +6 -0
  169. package/corpus/templates/mail/changelog/2026-10-20-gong-connection-details-now-match-the-call-data-available-in.md +6 -0
  170. package/corpus/templates/mail/changelog/2026-10-20-improve-gong-setup-with-a-click-to-connect-flow-and-permissi.md +6 -0
  171. package/corpus/templates/mail/changelog/2026-10-20-mail-lets-you-retry-when-it-cannot-check-your-gong-access.md +6 -0
  172. package/corpus/templates/mail/changelog/2026-10-20-mail-thread-cache-stays-isolated-when-mailbox-scope-changes.md +6 -0
  173. package/corpus/templates/mail/changelog/2026-10-20-the-inbox-and-recently-opened-threads-now-open-instantly-fro.md +6 -0
  174. package/corpus/templates/mail/server/handlers/emails.ts +9 -0
  175. package/corpus/templates/plan/.agents/skills/actions/references/action-fields.md +5 -3
  176. package/corpus/templates/plan/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  177. package/corpus/templates/plan/app/pages/PlansPage.tsx +173 -33
  178. package/corpus/templates/plan/changelog/2026-10-19-opening-a-comment-thread-puts-the-cursor-in-the-reply-box-an.md +6 -0
  179. package/corpus/templates/slides/.agents/skills/actions/references/action-fields.md +5 -3
  180. package/corpus/templates/slides/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  181. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +6 -42
  182. package/corpus/templates/slides/app/components/editor/FloatingToolbar.tsx +3 -7
  183. package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +5 -14
  184. package/corpus/templates/slides/app/i18n/en-US.ts +0 -2
  185. package/corpus/templates/slides/app/pages/DeckEditor.tsx +27 -33
  186. package/corpus/templates/slides/scripts/edit-fidelity/run.ts +1 -3
  187. package/corpus/templates/tasks/.agents/skills/actions/references/action-fields.md +5 -3
  188. package/package.json +1 -1
package/corpus/README.md CHANGED
@@ -32,4 +32,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core-corpus/corpu
32
32
 
33
33
  ## Generated Counts
34
34
 
35
- - template files: 13165
35
+ - template files: 13257
@@ -95,9 +95,9 @@ MCP clients see no `outputSchema` by default. Set `mcpOutputSchema: true` (requi
95
95
 
96
96
  `authorize` decides whether the caller may run the action at all. It wraps
97
97
  `run`, so it holds at **all six dispatch sites** — agent tool, HTTP, frontend
98
- hook, MCP, A2A, CLI — unlike `needsApproval`, which is honoured only in the
99
- agent loop. Use `authorize` for "not everyone may do this"; use `needsApproval`
100
- to ask a human to bless one call a permitted caller is already allowed to make.
98
+ hook, MCP, A2A, CLI. Use `authorize` for "not everyone may do this"; use
99
+ `needsApproval` to ask a human to bless one call a permitted caller is already
100
+ allowed to make.
101
101
 
102
102
  ```ts
103
103
  import { coachAccess } from "../lib/access.js"; // defineAppRoles(...)
@@ -140,4 +140,6 @@ export default defineAction({
140
140
 
141
141
  When the gate is truthy and the call isn't yet approved, the loop emits an `approval_required` event and **stops the turn — `run()` never executes**. A predicate gates conditionally (e.g. only external recipients) and **fails closed**: a throw is treated as "approval required". It receives its own copy of the prepared snapshot of the validated arguments, defaults applied, and the approval binds to the snapshot's hash, so an approved call runs only with the arguments that were approved (a Date binds by its time). The human approves via the chat UI's Approve affordance, which re-issues the turn with the call's `approvalKey`, and only then does the action run.
142
142
 
143
+ Approval is given in the app, so HTTP never grants it. The HTTP action route and the generated Workers entry run a gated call only for the proven app UI (a frontend call from the signed-in user, same origin, holding the short-lived UI capability; `isProvenUiRequest`, the proof `uiOnly` uses) or the app's MCP directory widget. Every other HTTP caller (bearer or API key, MCP OAuth token, org service token, A2A, extension iframe, mobile app) gets `409 approval_required`, and a service identity can never approve. The shared client presents the capability with every write from a signed-in session (minted once per user, reused for nine minutes, never for reads or public pages), so a gated button works on its first request. A call without proof gets `403 ui_capability_required`; the client then re-mints and retries once, which is only the fallback for an expired or rejected capability. Every surface evaluates the gate through `preparedCallNeedsApproval`, so a throwing predicate asks there too. A caller that must run unattended needs a different action, never a bypass of the gate.
144
+
143
145
  **Keep approvals rare** — the default is off and almost every action should leave it off. The canonical example is Mail's `send-email` (`needsApproval: true`). See the `security` skill and the Human Approval doc.
@@ -144,8 +144,32 @@ collection stays off until a property owner adds the service account that
144
144
  `get-content-collection` returns as `serviceAccount.email`; then set its
145
145
  `searchConsoleSites` and turn it on.
146
146
 
147
+ The `signups` step writes `signups_first_touch` per article: people who
148
+ first landed on the article and later signed up, on the day of that first
149
+ visit, so a window's days add up to its total. It is the only step that reads
150
+ the warehouse. It stays `not_configured` until `configure-content-collection`
151
+ sets its `signups` tables, `signupSource`, `signupsSince` and
152
+ `maxBytesBilled`. Measure the cap with `estimate-content-signups-cost` (a dry
153
+ run, free) and get the spend approved before turning the step on. Each run
154
+ dry-runs first, stops as `cost_limited` without querying when the estimate is
155
+ over the cap, and runs the join with that cap as BigQuery's billing limit.
156
+
157
+ The catalog's "Content" dashboard (`content-view`) is the reference for these
158
+ tables: start from its panels before writing new content SQL. Its Articles
159
+ panel shows why a metric is missing (`failed`, `not set up`, `still
160
+ arriving`) in a column next to the blank number; keep that pattern. The
161
+ first-party SQL policy rejects functions outside its allow-list (for example
162
+ `concat_ws`) and reads `CASE (` as a function call, so join a status column
163
+ instead of a scalar subquery inside `CASE`. Its trend chart uses wide rows
164
+ (`yKeys`), not `pivot`: a pivot fills a missing series with 0, so a day a
165
+ source didn't report would draw as a zero instead of a gap. Content is a
166
+ built-in: turning collection on with `configure-content-collection` lists it
167
+ in the organization's Dashboards at `content-view` (`contentDashboardId` in the
168
+ result). It is read-only; build on it with `duplicate-dashboard`.
169
+
147
170
  There is no dbt Semantic Layer panel source. For a Semantic Layer metric, answer in chat with `query-dbt-semantic-metric`; build a warehouse panel only on a table the owner names.
148
171
 
172
+
149
173
  AI-generated first-party panels are dashboard-time-bound by default. Set
150
174
  `config.timeScope` to `"dashboard"` and include the matching dashboard time
151
175
  filter in the SQL. The allowed values are:
@@ -467,8 +491,9 @@ visualizations outside these contracts may remain Custom Blocks.
467
491
 
468
492
  ## Sigma workbook import
469
493
 
470
- The dashboard menu opens the Sigma import flow, where users can browse and
471
- preview workbooks. Imports remain disabled for elements whose source project
494
+ The dashboard menu opens the Sigma import flow: a searchable table of every
495
+ workbook the user can see in Sigma, with the owner of each migrated one and a
496
+ Migrate button on the rest. Imports remain disabled for elements whose source project
472
497
  Analytics cannot verify: Sigma's connection API reports provider type and
473
498
  metadata but does not expose a BigQuery project identifier. Do not call
474
499
  `migrate-sigma-workbook` for an unverified source or treat an Analytics
@@ -489,6 +514,16 @@ encrypted organization secrets and links them to an Analytics-only connection.
489
514
  Credential values must never pass through agent chat, action arguments, logs,
490
515
  source, or screenshots.
491
516
 
517
+ Each Sigma workbook migrates to at most one Analytics dashboard per
518
+ organization. `list-sigma-workbooks` reads migration status only for workbooks
519
+ the caller's Sigma connection returns, so status follows Sigma access. For a
520
+ migrated workbook it returns the dashboard id, link, and owner email and
521
+ display name to every org member, and the title and `canOpen: true` only when the caller can open
522
+ the dashboard. Point users at that dashboard, or its owner for access, instead
523
+ of importing again. Anyone whose Sigma login can open a workbook may retry its
524
+ failed import or take over an import stuck for ten minutes; only the person who
525
+ migrated a workbook may re-import it.
526
+
492
527
  If Sigma adds verifiable source mapping in the future, the planned import will
493
528
  create a private snapshot in the user's personal `Sigma` folder. Other source
494
529
  types and unsupported elements will become review callouts. Preserve migration
@@ -768,6 +803,10 @@ When the user asks to change an existing panel on the open dashboard:
768
803
  `verified: false`, an error, or the user saying the change is not visible,
769
804
  call `inspect-dashboard-panel` for that panel, fix what it reports, and
770
805
  verify again. Never describe a visible change you have not verified.
806
+ An edit that changes a panel's query also returns `cost`: what one refresh
807
+ of the changed panels scans and costs at the default filters, and per month
808
+ for the dashboard's email reports. The summary repeats it; when it says the
809
+ cost is over a threshold, tell the user the number.
771
810
  `update-dashboard`, `compose-dashboard`, and `restore-dashboard-revision`
772
811
  return the same fields. Editing a dashboard you can only view fails with
773
812
  `dashboard_forbidden` before any SQL runs.
@@ -114,8 +114,10 @@ After cutover, an org admin can point charts at a scheduled warehouse events
114
114
  model with `configure-first-party-events-model`; run `check` first, which
115
115
  dry-runs as the app and saves nothing. With a model set, a first-party query
116
116
  reads `analytics_events` from the model only when its WHERE filters
117
- `event_name` to named events (`= 'x'` or `IN (...)`, ANDed, no OR or NOT),
118
- because the model has no `http.response` rows; its result carries
117
+ `event_name` to named events (`= 'x'` or `IN (...)`) in a top-level AND
118
+ conjunct, or in a parenthesized OR whose every branch does, because the model
119
+ has no `http.response` rows. Other parenthesized conditions may use OR and
120
+ NOT; an OR, NOT, or CASE at the WHERE's own level keeps the query raw; its result carries
119
121
  `eventsSource` and `sourceGeneration` (the model build it read). Unfiltered
120
122
  queries, `SELECT *`, and queries that name `public_key_id` or `context`, read
121
123
  `http.response`, or read `properties` other than through `JSON_VALUE`,
@@ -0,0 +1,10 @@
1
+ // AUTO-GENERATED by @agent-native/core deploy build
2
+ import { markDefaultPluginProvided } from "@agent-native/core/server/edge";
3
+
4
+ const pluginStems = ["agent-chat","analytics-alert-jobs","analytics-rollup-backfill-jobs","auth","collab","config","content-metrics-collection-jobs","core-routes","creative-context","dashboard-report-jobs","db","feature-flags","labs","onboarding","otel","session-replay-retention-jobs","transactional-emails","uptime-monitor-jobs"];
5
+
6
+ export default function markBuildDiscoveredPlugins(nitroApp) {
7
+ for (const stem of pluginStems) {
8
+ markDefaultPluginProvided(nitroApp, stem);
9
+ }
10
+ }
@@ -1,7 +1,9 @@
1
1
  import { defineAction, fail } from "@agent-native/core/action";
2
2
  import {
3
+ getRequestContext,
3
4
  getRequestOrgId,
4
5
  getRequestUserEmail,
6
+ runWithRequestContext,
5
7
  } from "@agent-native/core/server";
6
8
  import { getOrgSetting, mutateOrgSetting } from "@agent-native/core/settings";
7
9
  import { z } from "zod";
@@ -110,8 +112,15 @@ export default defineAction({
110
112
  });
111
113
  let outcome: SourceIndexRunOutcome;
112
114
  try {
113
- const { bundle: builtBundle } =
114
- await buildDbtSourceIndexFromGitHub(repository);
115
+ const { bundle: builtBundle } = await runWithRequestContext(
116
+ {
117
+ ...(getRequestContext() ?? {}),
118
+ userEmail: admin.userEmail,
119
+ orgId,
120
+ credentialScope: "org",
121
+ },
122
+ () => buildDbtSourceIndexFromGitHub(repository),
123
+ );
115
124
  // Computed before the write so a bad source fails the run instead of
116
125
  // leaving a stored index with no recorded revisions.
117
126
  // Check the new build before the write, then report its final source IDs
@@ -5,9 +5,12 @@ import {
5
5
  } from "@agent-native/core/server";
6
6
  import { z } from "zod";
7
7
 
8
+ import { installBuiltinDashboardOnce } from "../server/lib/builtin-dashboards.js";
8
9
  import {
9
10
  contentCollectionConfigSchema,
10
11
  searchConsoleSiteSchema,
12
+ signupsSettingsUpdateSchema,
13
+ storedSearchConsoleSiteSchema,
11
14
  } from "../server/lib/content-metrics/config.js";
12
15
  import { readContentCollectionStatus } from "../server/lib/content-metrics/status.js";
13
16
  import {
@@ -17,6 +20,8 @@ import {
17
20
  import { requireAnalyticsAdminContext } from "../server/lib/db-admin-connections.js";
18
21
  import { cliBoolean } from "./schema-helpers.js";
19
22
 
23
+ const CONTENT_VIEW_DASHBOARD_ID = "content-view";
24
+
20
25
  export default defineAction({
21
26
  mcpAnnotations: {
22
27
  readOnlyHint: false,
@@ -24,7 +29,7 @@ export default defineAction({
24
29
  openWorldHint: false,
25
30
  },
26
31
  description:
27
- "Turn the organization's daily Content view collection on or off and choose its steps. Requires an organization owner or admin. The caller becomes the stored owner: the daily job resolves provider connections as them. Omitted step settings keep their saved values.",
32
+ "Turn the organization's daily Content view collection on or off and choose its steps. Requires an organization owner or admin. The caller becomes the stored owner: the daily job resolves provider connections as them. Omitted step settings keep their saved values. Turning collection on also lists the built-in Content dashboard in the organization's Dashboards; the result's contentDashboardId opens it at /dashboards/<id>.",
28
33
  schema: z.object({
29
34
  enabled: cliBoolean.describe("Whether the daily collection runs."),
30
35
  builderBlog: cliBoolean
@@ -39,15 +44,20 @@ export default defineAction({
39
44
  "Collect daily Search Console clicks and impressions. Turn on only after a property owner has given the service account access.",
40
45
  ),
41
46
  searchConsoleSites: z
42
- .array(searchConsoleSiteSchema)
47
+ .array(storedSearchConsoleSiteSchema)
43
48
  .max(20)
44
49
  .optional()
45
50
  .describe(
46
51
  'Search Console properties to collect, replacing the saved list, e.g. [{"siteUrl":"sc-domain:example.com","pathPrefix":"/blog/"}].',
47
52
  ),
53
+ signups: signupsSettingsUpdateSchema
54
+ .optional()
55
+ .describe(
56
+ "Per-article signups settings, merged into the saved ones: enabled, signupEventsTable and firstPageviewsTable (project.dataset.table), signupSource (its signups are also counted on their own, as Source signups), signupsSince (YYYY-MM-DD) and maxBytesBilled. The step stays not set up until all are set; measure maxBytesBilled with estimate-content-signups-cost, and get it approved before turning the step on, since every run scans the warehouse.",
57
+ ),
48
58
  }),
49
59
  run: async (
50
- { enabled, builderBlog, ga4, searchConsole, searchConsoleSites },
60
+ { enabled, builderBlog, ga4, searchConsole, searchConsoleSites, signups },
51
61
  ctx,
52
62
  ) => {
53
63
  const admin = await requireAnalyticsAdminContext({
@@ -67,8 +77,23 @@ export default defineAction({
67
77
  enabled: searchConsole ?? saved.searchConsole.enabled,
68
78
  sites: searchConsoleSites ?? saved.searchConsole.sites,
69
79
  },
80
+ signups: { ...saved.signups, ...signups },
70
81
  });
82
+ if (enabled && config.searchConsole.enabled) {
83
+ z.array(searchConsoleSiteSchema)
84
+ .max(20)
85
+ .parse(config.searchConsole.sites);
86
+ }
87
+ if (enabled) {
88
+ await installBuiltinDashboardOnce(
89
+ { email: admin.userEmail, orgId: admin.orgId },
90
+ CONTENT_VIEW_DASHBOARD_ID,
91
+ );
92
+ }
71
93
  await writeContentCollection(scope, { enabled, config });
72
- return readContentCollectionStatus(admin.orgId, admin.userEmail);
94
+ return {
95
+ ...(await readContentCollectionStatus(admin.orgId, admin.userEmail)),
96
+ contentDashboardId: enabled ? CONTENT_VIEW_DASHBOARD_ID : null,
97
+ };
73
98
  },
74
99
  });
@@ -33,7 +33,9 @@ function utcDateDaysAgo(days: number): string {
33
33
  }
34
34
 
35
35
  // Dry runs only: BigQuery plans the query and reports the bytes it would
36
- // scan, but runs and bills nothing.
36
+ // scan, but runs and bills nothing. Both read every projected column: BigQuery
37
+ // bills only the columns a query reads, so COUNT(*) would report almost nothing
38
+ // while a chart can read any of them.
37
39
  async function checkModel(
38
40
  table: string,
39
41
  signal?: AbortSignal,
@@ -47,7 +49,7 @@ async function checkModel(
47
49
  }
48
50
  const projection = `(${firstPartyEventsModelProjection(table)}) AS analytics_events`;
49
51
  const recent = await dryRunQuerySchema(
50
- `SELECT COUNT(*) AS events FROM ${projection} WHERE event_date >= DATE '${utcDateDaysAgo(1)}'`,
52
+ `SELECT * FROM ${projection} WHERE event_date = DATE '${utcDateDaysAgo(1)}'`,
51
53
  { signal },
52
54
  );
53
55
  if (recent.error) {
@@ -60,10 +62,10 @@ async function checkModel(
60
62
  }
61
63
  // Eligible chart SQL need not bound event_date, so a model that refuses an
62
64
  // unbounded read (a required partition filter, say) would fail those charts.
63
- const full = await dryRunQuerySchema(
64
- `SELECT COUNT(*) AS events FROM ${projection}`,
65
- { signal },
66
- );
65
+ // guard:allow-unbounded-read — dry run: BigQuery plans it and bills nothing.
66
+ const full = await dryRunQuerySchema(`SELECT * FROM ${projection}`, {
67
+ signal,
68
+ });
67
69
  if (full.error) {
68
70
  return {
69
71
  status: "unavailable",
@@ -91,7 +93,7 @@ export default defineAction({
91
93
  openWorldHint: true,
92
94
  },
93
95
  description:
94
- "Check or set the warehouse events model that first-party Analytics charts read instead of the raw event table. `check` (any org member) reads the model's table metadata and dry-runs the chart projection against it as the app, reporting whether the app can query it and the bytes a one-day and a full read would scan; it runs and bills no query. `set` (org admin, needs approval) runs the same check and saves the model only if it passes. `clear` (org admin, needs approval) sends charts back to the raw table. Charts that read columns or events the model lacks, alerts, and onboarding journeys keep reading the raw table either way.",
96
+ "Check or set the warehouse events model that first-party Analytics charts read instead of the raw event table. `check` (any org member) reads the model's table metadata and dry-runs the chart projection against it as the app, reporting whether the app can query it and the most bytes one read of yesterday's full UTC day, or of the whole model, could scan (every column; a chart that reads the model more than once, such as a self-join, scans that much per read); it runs and bills no query. `set` (org admin, needs approval) runs the same check and saves the model only if it passes. `clear` (org admin, needs approval) sends charts back to the raw table. Charts that read columns or events the model lacks, alerts, and onboarding journeys keep reading the raw table either way.",
95
97
  schema: z.object({
96
98
  mode: z.enum(["check", "set", "clear"]),
97
99
  table: z
@@ -22,6 +22,10 @@ export default defineAction({
22
22
  .optional()
23
23
  .describe("Human label for this key, e.g. 'Hosted templates'."),
24
24
  }),
25
+ // It creates, changes, or revokes a credential or who may use one: every
26
+ // call needs the user's approval, and "always allow" is not offered.
27
+ needsApproval: true,
28
+ allowPersistentApproval: false,
25
29
  run: async (args) => {
26
30
  return createAnalyticsPublicKey(resolveScope(), args.name ?? "Default key");
27
31
  },
@@ -0,0 +1,72 @@
1
+ import { defineAction, fail } from "@agent-native/core/action";
2
+ import {
3
+ getRequestOrgId,
4
+ getRequestUserEmail,
5
+ } from "@agent-native/core/server";
6
+ import { z } from "zod";
7
+
8
+ import { dryRunQuerySchema } from "../server/lib/bigquery.js";
9
+ import { ContentStepStop } from "../server/lib/content-metrics/step.js";
10
+ import {
11
+ buildSignupsQuery,
12
+ signupsTables,
13
+ } from "../server/lib/content-metrics/steps/signups.js";
14
+ import { readContentCollection } from "../server/lib/content-metrics/store.js";
15
+ import { requireAnalyticsAdminContext } from "../server/lib/db-admin-connections.js";
16
+
17
+ export default defineAction({
18
+ description:
19
+ "Dry-run today's per-article signups join for the organization's Content view and return the bytes BigQuery estimates it would scan, next to the saved maxBytesBilled cap. A dry run scans and bills nothing. Requires an organization owner or admin and the signups warehouse settings from configure-content-collection; use the estimate to choose maxBytesBilled before turning the signups step on.",
20
+ schema: z.object({}),
21
+ readOnly: true,
22
+ run: async (_args, ctx) => {
23
+ const admin = await requireAnalyticsAdminContext({
24
+ userEmail: getRequestUserEmail() || ctx?.userEmail,
25
+ orgId: getRequestOrgId() || ctx?.orgId || null,
26
+ });
27
+ const collection = await readContentCollection({
28
+ ownerEmail: admin.userEmail,
29
+ orgId: admin.orgId,
30
+ });
31
+ if (!collection) {
32
+ fail("Content collection isn't set up for this organization yet.", {
33
+ errorCode: "content_collection_not_configured",
34
+ statusCode: 412,
35
+ });
36
+ }
37
+ const signups = collection.config.signups;
38
+ let tables: ReturnType<typeof signupsTables>;
39
+ try {
40
+ tables = signupsTables(signups);
41
+ } catch (error) {
42
+ if (!(error instanceof ContentStepStop)) throw error;
43
+ fail(error.message, {
44
+ errorCode: error.failure.errorCode,
45
+ statusCode: 412,
46
+ });
47
+ }
48
+ const query = buildSignupsQuery(tables, new Date());
49
+ const estimate = await dryRunQuerySchema(query.sql);
50
+ if (estimate.error) {
51
+ fail(`The signups dry run failed: ${estimate.error}`, {
52
+ errorCode: "signups_dry_run_failed",
53
+ statusCode: 502,
54
+ });
55
+ }
56
+ if (estimate.totalBytesProcessed === undefined) {
57
+ fail("BigQuery returned no byte estimate for the signups join.", {
58
+ errorCode: "signups_estimate_unavailable",
59
+ statusCode: 502,
60
+ });
61
+ }
62
+ return {
63
+ estimatedBytes: estimate.totalBytesProcessed,
64
+ maxBytesBilled: signups.maxBytesBilled,
65
+ withinCap:
66
+ signups.maxBytesBilled === null
67
+ ? null
68
+ : estimate.totalBytesProcessed <= signups.maxBytesBilled,
69
+ window: { startDate: query.startDate, endDate: query.endDate },
70
+ };
71
+ },
72
+ });
@@ -11,141 +11,35 @@ import { isSafeDashboardId } from "../server/lib/dashboard-id";
11
11
  import {
12
12
  pageDashboardConfig,
13
13
  resolveDashboardPanelQueries,
14
- type ResolvedDashboardPanelQuery,
15
14
  } from "../server/lib/dashboard-panel-verification";
16
- import { countEnabledDashboardReportSubscriptions } from "../server/lib/dashboard-report-subscriptions";
17
15
  import { loadDashboardSeed } from "../server/lib/dashboard-seeds";
18
16
  import { getDashboard } from "../server/lib/dashboards-store";
19
17
  import type { AnalyticsScope } from "../server/lib/first-party-analytics";
20
18
  import {
21
- DAYS_PER_MONTH,
19
+ describeEstimate,
20
+ describeReportSchedules,
22
21
  estimateBigQuerySql,
23
22
  estimateFirstPartySql,
24
- estimateResolvedPanel,
23
+ estimateResolvedPanels,
25
24
  formatBytes,
26
25
  formatUsd,
27
26
  ON_DEMAND_USD_PER_TIB,
28
27
  QUERY_COST_CAVEATS,
29
28
  QUERY_COST_THRESHOLDS,
29
+ scheduledDashboardRuns,
30
30
  totalCost,
31
31
  type PanelCostEstimate,
32
- type QueryCostEstimate,
33
32
  } from "../server/lib/query-cost-estimate";
34
33
  import { clipHead } from "../shared/panel-render-contract";
35
34
 
36
35
  const MAX_PANELS = 60;
37
- const DRY_RUN_CONCURRENCY = 6;
38
36
  /** Stop starting dry runs here so the action returns before its 55 s timeout. */
39
37
  const DASHBOARD_DEADLINE_MS = 40_000;
40
38
 
41
- function timedOutPanel(
42
- panel: { panelId: string; title: string; source: string },
43
- error: string,
44
- ): PanelCostEstimate {
45
- return {
46
- panelId: panel.panelId,
47
- title: panel.title,
48
- source: panel.source,
49
- status: "failed",
50
- failure: "timeout",
51
- error,
52
- };
53
- }
54
-
55
- /**
56
- * Estimate one panel, but give up when the dashboard deadline passes, even
57
- * mid-way through credential, table, or backend lookups, so finished panels
58
- * still come back before the action's own timeout.
59
- */
60
- async function estimatePanelBefore(
61
- deadline: number,
62
- panel: ResolvedDashboardPanelQuery,
63
- scope: AnalyticsScope,
64
- runsPerMonth: number | null,
65
- signal?: AbortSignal,
66
- ): Promise<PanelCostEstimate> {
67
- const remaining = deadline - Date.now();
68
- if (remaining <= 0) {
69
- return timedOutPanel(
70
- panel,
71
- "Skipped: this request ran out of time. Estimate this panel on its own with panelId.",
72
- );
73
- }
74
- const controller = new AbortController();
75
- const forwardAbort = () => controller.abort(signal?.reason);
76
- if (signal?.aborted) forwardAbort();
77
- signal?.addEventListener("abort", forwardAbort, { once: true });
78
- let timer: ReturnType<typeof setTimeout> | undefined;
79
- const timedOut = new Promise<PanelCostEstimate>((resolve) => {
80
- timer = setTimeout(() => {
81
- resolve(
82
- timedOutPanel(
83
- panel,
84
- "Stopped: this panel's dry run didn't finish in time. Estimate it on its own with panelId.",
85
- ),
86
- );
87
- controller.abort();
88
- }, remaining);
89
- });
90
- try {
91
- return await Promise.race([
92
- estimateResolvedPanel(panel, scope, runsPerMonth, controller.signal),
93
- timedOut,
94
- ]);
95
- } finally {
96
- clearTimeout(timer);
97
- signal?.removeEventListener("abort", forwardAbort);
98
- }
99
- }
100
-
101
- async function mapPool<T, R>(
102
- items: readonly T[],
103
- concurrency: number,
104
- fn: (item: T) => Promise<R>,
105
- ): Promise<R[]> {
106
- const results: R[] = Array(items.length);
107
- let next = 0;
108
- const worker = async () => {
109
- while (next < items.length) {
110
- const index = next++;
111
- results[index] = await fn(items[index]);
112
- }
113
- };
114
- await Promise.all(
115
- Array.from({ length: Math.min(concurrency, items.length) }, worker),
116
- );
117
- return results;
118
- }
119
-
120
39
  function sentence(text: string): string {
121
40
  return /[.!?]$/.test(text) ? text : `${text}.`;
122
41
  }
123
42
 
124
- function describeEstimate(estimate: QueryCostEstimate): string {
125
- if (
126
- estimate.status === "not_bigquery" ||
127
- estimate.status === "not_estimated"
128
- ) {
129
- return estimate.reason;
130
- }
131
- if (estimate.status === "failed") {
132
- return `no estimate (${estimate.failure}): ${estimate.error}`;
133
- }
134
- const monthly =
135
- estimate.usdPerMonth === null
136
- ? "not scheduled"
137
- : `~${formatUsd(estimate.usdPerMonth)}/month over ${estimate.runsPerMonth} scheduled runs`;
138
- const flags = [
139
- estimate.overPerRunThreshold
140
- ? `over the ${formatUsd(QUERY_COST_THRESHOLDS.perRunUsd)}/run threshold`
141
- : null,
142
- estimate.overPerMonthThreshold
143
- ? `over the ${formatUsd(QUERY_COST_THRESHOLDS.perMonthUsd)}/month threshold`
144
- : null,
145
- ].filter(Boolean);
146
- return `~${formatBytes(estimate.estimatedBytes)}, ${formatUsd(estimate.usdPerRun)} per run, ${monthly}${flags.length ? ` (${flags.join("; ")})` : ""}`;
147
- }
148
-
149
43
  export default defineAction({
150
44
  description:
151
45
  "Free cost estimate for BigQuery SQL. Dry-runs the query through Analytics' own BigQuery connection (BigQuery reads and bills nothing) and returns the bytes it would scan, dollars per run at on-demand pricing, and dollars per month for scheduled runs. " +
@@ -312,20 +206,15 @@ export default defineAction({
312
206
  );
313
207
  }
314
208
 
315
- const reportSchedules = await countEnabledDashboardReportSubscriptions(
316
- dashboardId,
317
- { email: userEmail, orgId },
318
- );
319
- const runsPerMonth =
320
- args.runsPerMonth ??
321
- (reportSchedules > 0 ? reportSchedules * DAYS_PER_MONTH : null);
322
- const estimated = selected.slice(0, MAX_PANELS);
323
- const deadline = startedAt + DASHBOARD_DEADLINE_MS;
324
- const panels: PanelCostEstimate[] = await mapPool(
325
- estimated,
326
- DRY_RUN_CONCURRENCY,
327
- (panel) =>
328
- estimatePanelBefore(deadline, panel, scope, runsPerMonth, signal),
209
+ const schedule = await scheduledDashboardRuns(dashboardId, scope);
210
+ const reportSchedules = schedule.reportSchedules;
211
+ const runsPerMonth = args.runsPerMonth ?? schedule.runsPerMonth;
212
+ const panels: PanelCostEstimate[] = await estimateResolvedPanels(
213
+ selected.slice(0, MAX_PANELS),
214
+ scope,
215
+ runsPerMonth,
216
+ startedAt + DASHBOARD_DEADLINE_MS,
217
+ signal,
329
218
  );
330
219
  const total = totalCost(panels);
331
220
  const flagged = panels.filter(
@@ -358,16 +247,10 @@ export default defineAction({
358
247
  : args.runsPerMonth === undefined
359
248
  ? `~${formatUsd(total.usdPerMonth)}/month if its email reports use these filters`
360
249
  : `~${formatUsd(total.usdPerMonth)}/month in scheduled runs`;
361
- const totalFlags = {
362
- overPerRunThreshold: total.usdPerRun > QUERY_COST_THRESHOLDS.perRunUsd,
363
- overPerMonthThreshold:
364
- total.usdPerMonth !== null &&
365
- total.usdPerMonth > QUERY_COST_THRESHOLDS.perMonthUsd,
366
- };
367
250
  const totalOver = [
368
- totalFlags.overPerRunThreshold &&
251
+ total.overPerRunThreshold &&
369
252
  `the ${formatUsd(QUERY_COST_THRESHOLDS.perRunUsd)}/run`,
370
- totalFlags.overPerMonthThreshold &&
253
+ total.overPerMonthThreshold &&
371
254
  `the ${formatUsd(QUERY_COST_THRESHOLDS.perMonthUsd)}/month`,
372
255
  ].filter(Boolean);
373
256
 
@@ -383,11 +266,10 @@ export default defineAction({
383
266
  args.runsPerMonth !== undefined
384
267
  ? "caller"
385
268
  : reportSchedules > 0
386
- ? `${reportSchedules} enabled daily email report${reportSchedules === 1 ? "" : "s"} ${orgId ? "in your org" : "of yours"}`
269
+ ? describeReportSchedules(reportSchedules, orgId)
387
270
  : "none",
388
271
  total: {
389
272
  ...total,
390
- ...totalFlags,
391
273
  partial,
392
274
  scope: onePanel ? "panel" : "dashboard",
393
275
  },