@agent-native/core 0.90.11 → 0.91.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 (315) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +22 -0
  3. package/corpus/core/docs/content/toolkit-settings.mdx +56 -3
  4. package/corpus/core/package.json +2 -1
  5. package/corpus/core/src/agent/run-ownership.ts +29 -1
  6. package/corpus/core/src/changelog/parse.ts +78 -0
  7. package/corpus/core/src/chat-threads/schema.ts +43 -0
  8. package/corpus/core/src/chat-threads/store.ts +144 -12
  9. package/corpus/core/src/cli/workspace-dev.ts +25 -5
  10. package/corpus/core/src/client/AgentPanel.tsx +79 -14
  11. package/corpus/core/src/client/AssistantChat.tsx +359 -425
  12. package/corpus/core/src/client/analytics-session.ts +70 -0
  13. package/corpus/core/src/client/analytics.ts +175 -49
  14. package/corpus/core/src/client/chat/message-components.tsx +22 -2
  15. package/corpus/core/src/client/components/ui/message-scroller.tsx +131 -0
  16. package/corpus/core/src/client/conversation/AgentConversation.tsx +63 -41
  17. package/corpus/core/src/client/error-capture.ts +616 -0
  18. package/corpus/core/src/client/index.ts +12 -0
  19. package/corpus/core/src/client/session-replay.ts +57 -7
  20. package/corpus/core/src/client/settings/SettingsPanel.tsx +123 -1
  21. package/corpus/core/src/client/settings/SettingsTabsPage.tsx +292 -45
  22. package/corpus/core/src/client/settings/index.ts +1 -0
  23. package/corpus/core/src/client/sharing/ShareButton.tsx +14 -11
  24. package/corpus/core/src/client/sharing/ShareDialog.tsx +13 -10
  25. package/corpus/core/src/mcp/builtin-tools.ts +29 -2
  26. package/corpus/core/src/server/agent-chat-plugin.ts +174 -79
  27. package/corpus/core/src/server/onboarding-html.ts +28 -16
  28. package/corpus/core/src/sharing/actions/list-resource-shares.ts +45 -1
  29. package/corpus/core/src/sharing/actions/share-resource.ts +9 -0
  30. package/corpus/core/src/templates/workspace-core/.agents/skills/agent-native-toolkit/SKILL.md +4 -2
  31. package/corpus/core/src/templates/workspace-core/.agents/skills/changelog/SKILL.md +9 -6
  32. package/corpus/core/src/vite/client.ts +217 -35
  33. package/corpus/templates/analytics/.agents/skills/adhoc-analysis/SKILL.md +8 -7
  34. package/corpus/templates/analytics/.agents/skills/analysis-workspace/SKILL.md +14 -10
  35. package/corpus/templates/analytics/.agents/skills/bigquery/SKILL.md +15 -15
  36. package/corpus/templates/analytics/.agents/skills/cross-source-analysis/SKILL.md +1 -0
  37. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +38 -22
  38. package/corpus/templates/analytics/.agents/skills/data-programs/SKILL.md +114 -61
  39. package/corpus/templates/analytics/.agents/skills/data-querying/SKILL.md +1 -0
  40. package/corpus/templates/analytics/.agents/skills/gong/SKILL.md +7 -7
  41. package/corpus/templates/analytics/.agents/skills/hubspot/SKILL.md +1 -0
  42. package/corpus/templates/analytics/.agents/skills/prometheus/SKILL.md +1 -1
  43. package/corpus/templates/analytics/.builder/skills/bigquery/SKILL.md +17 -17
  44. package/corpus/templates/analytics/.builder/skills/gcloud/SKILL.md +5 -5
  45. package/corpus/templates/analytics/.builder/skills/github/SKILL.md +7 -7
  46. package/corpus/templates/analytics/.builder/skills/gong/SKILL.md +12 -12
  47. package/corpus/templates/analytics/.builder/skills/grafana/SKILL.md +7 -7
  48. package/corpus/templates/analytics/.builder/skills/sentry/SKILL.md +7 -7
  49. package/corpus/templates/analytics/.builder/skills/slack/SKILL.md +6 -6
  50. package/corpus/templates/analytics/.builder/skills/stripe/SKILL.md +7 -7
  51. package/corpus/templates/analytics/AGENTS.md +47 -2
  52. package/corpus/templates/analytics/actions/add-status-page-monitor.ts +36 -0
  53. package/corpus/templates/analytics/actions/capture-test-error.ts +33 -0
  54. package/corpus/templates/analytics/actions/delete-monitor.ts +24 -0
  55. package/corpus/templates/analytics/actions/delete-status-page.ts +23 -0
  56. package/corpus/templates/analytics/actions/get-error-issue.ts +36 -0
  57. package/corpus/templates/analytics/actions/get-monitor-stats.ts +55 -0
  58. package/corpus/templates/analytics/actions/get-monitor.ts +27 -0
  59. package/corpus/templates/analytics/actions/get-public-status-page.ts +41 -0
  60. package/corpus/templates/analytics/actions/get-status-page.ts +29 -0
  61. package/corpus/templates/analytics/actions/list-error-issues.ts +40 -0
  62. package/corpus/templates/analytics/actions/list-monitors.ts +21 -0
  63. package/corpus/templates/analytics/actions/list-status-pages.ts +21 -0
  64. package/corpus/templates/analytics/actions/match-error-issues.ts +51 -0
  65. package/corpus/templates/analytics/actions/navigate.ts +58 -4
  66. package/corpus/templates/analytics/actions/remove-status-page-monitor.ts +22 -0
  67. package/corpus/templates/analytics/actions/reorder-status-page-monitors.ts +25 -0
  68. package/corpus/templates/analytics/actions/resolve-error-issue.ts +38 -0
  69. package/corpus/templates/analytics/actions/run-monitor-check.ts +23 -0
  70. package/corpus/templates/analytics/actions/save-analysis.ts +1 -0
  71. package/corpus/templates/analytics/actions/save-monitor.ts +145 -0
  72. package/corpus/templates/analytics/actions/save-status-page.ts +47 -0
  73. package/corpus/templates/analytics/actions/view-screen.ts +212 -0
  74. package/corpus/templates/analytics/app/components/layout/Header.tsx +1 -0
  75. package/corpus/templates/analytics/app/components/layout/Layout.tsx +10 -1
  76. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +134 -39
  77. package/corpus/templates/analytics/app/components/monitoring/PublicStatusView.tsx +351 -0
  78. package/corpus/templates/analytics/app/components/monitoring/ResponseTimeChart.tsx +174 -0
  79. package/corpus/templates/analytics/app/components/monitoring/UptimeStatCards.tsx +85 -0
  80. package/corpus/templates/analytics/app/components/monitoring/UptimeTimelineBars.tsx +106 -0
  81. package/corpus/templates/analytics/app/components/monitoring/chart-utils.ts +101 -0
  82. package/corpus/templates/analytics/app/components/monitoring/index.ts +28 -0
  83. package/corpus/templates/analytics/app/components/monitoring/types.ts +42 -0
  84. package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +34 -0
  85. package/corpus/templates/analytics/app/i18n/zh-TW.ts +6 -0
  86. package/corpus/templates/analytics/app/i18n-data.ts +33 -0
  87. package/corpus/templates/analytics/app/pages/DataSources.tsx +28 -0
  88. package/corpus/templates/analytics/app/pages/Settings.tsx +77 -9
  89. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +3 -3
  90. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/dashboard-layout.ts +7 -0
  91. package/corpus/templates/analytics/app/pages/monitoring/ErrorsPanel.tsx +205 -0
  92. package/corpus/templates/analytics/app/pages/monitoring/MonitoringPage.tsx +100 -0
  93. package/corpus/templates/analytics/app/pages/monitoring/UptimePanel.tsx +475 -0
  94. package/corpus/templates/analytics/app/pages/monitoring/errors/IssueDetail.tsx +372 -0
  95. package/corpus/templates/analytics/app/pages/monitoring/errors/IssueList.tsx +360 -0
  96. package/corpus/templates/analytics/app/pages/monitoring/errors/Sparkline.tsx +55 -0
  97. package/corpus/templates/analytics/app/pages/monitoring/errors/i18n.ts +106 -0
  98. package/corpus/templates/analytics/app/pages/monitoring/errors/types.ts +75 -0
  99. package/corpus/templates/analytics/app/pages/monitoring/errors/utils.ts +132 -0
  100. package/corpus/templates/analytics/app/pages/monitoring/uptime/MonitorDetail.tsx +535 -0
  101. package/corpus/templates/analytics/app/pages/monitoring/uptime/MonitorFormPage.tsx +1026 -0
  102. package/corpus/templates/analytics/app/pages/monitoring/uptime/MonitorList.tsx +311 -0
  103. package/corpus/templates/analytics/app/pages/monitoring/uptime/i18n.ts +235 -0
  104. package/corpus/templates/analytics/app/pages/monitoring/uptime/status-pages/StatusPageEditor.tsx +661 -0
  105. package/corpus/templates/analytics/app/pages/monitoring/uptime/status-pages/StatusPagesView.tsx +355 -0
  106. package/corpus/templates/analytics/app/pages/monitoring/uptime/status-pages/i18n.ts +106 -0
  107. package/corpus/templates/analytics/app/pages/monitoring/uptime/status-pages/slug.ts +24 -0
  108. package/corpus/templates/analytics/app/pages/monitoring/uptime/status-pages/types.ts +20 -0
  109. package/corpus/templates/analytics/app/pages/monitoring/uptime/status-summary.ts +87 -0
  110. package/corpus/templates/analytics/app/pages/monitoring/uptime/types.ts +161 -0
  111. package/corpus/templates/analytics/app/pages/monitoring/uptime/utils.ts +210 -0
  112. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +76 -1
  113. package/corpus/templates/analytics/app/pages/sessions/SessionDevToolsPanel.tsx +74 -9
  114. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +197 -20
  115. package/corpus/templates/analytics/app/root.tsx +33 -1
  116. package/corpus/templates/analytics/app/routes/monitoring._index.tsx +10 -0
  117. package/corpus/templates/analytics/app/routes/status.$slug.tsx +118 -0
  118. package/corpus/templates/analytics/changelog/2026-07-08-add-uptime-monitors-that-ping-your-urls-and-alert-you-when-t.md +6 -0
  119. package/corpus/templates/analytics/changelog/2026-07-08-analytics-chats-keep-large-data-dictionaries-compact-so-prov.md +6 -0
  120. package/corpus/templates/analytics/changelog/2026-07-08-create-public-status-pages-that-share-the-live-health-of-cho.md +6 -0
  121. package/corpus/templates/analytics/changelog/2026-07-08-dashboard-extension-panels-use-clearer-open-copy.md +6 -0
  122. package/corpus/templates/analytics/changelog/2026-07-08-error-capture.md +6 -0
  123. package/corpus/templates/analytics/changelog/2026-07-08-fixed-single-panel-dashboard-drags-so-they-no-longer-rewrite.md +6 -0
  124. package/corpus/templates/analytics/changelog/2026-07-08-jump-straight-from-an-error-in-a-session-recording-to-its-fu.md +6 -0
  125. package/corpus/templates/analytics/changelog/2026-07-08-monitor-detail-and-list-now-show-colorful-uptime-timelines-r.md +6 -0
  126. package/corpus/templates/analytics/changelog/2026-07-08-pinned-ask-chats-now-show-a-pin-icon-in-the-sidebar.md +6 -0
  127. package/corpus/templates/analytics/changelog/2026-07-08-publish-a-public-uptime-status-page-with-colorful-uptime-tim.md +6 -0
  128. package/corpus/templates/analytics/changelog/2026-07-08-sessions-now-hide-visitor-emails-in-demo-mode-and-show-clean.md +6 -0
  129. package/corpus/templates/analytics/changelog/2026-07-08-settings-are-cleaner-searchable-and-alerts-have-their-own-tab.md +5 -0
  130. package/corpus/templates/analytics/changelog/2026-07-08-sidebar-sections-stay-collapsed-until-opened.md +5 -0
  131. package/corpus/templates/analytics/changelog/2026-07-08-the-uptime-monitor-list-now-has-a-current-status-overview-wi.md +6 -0
  132. package/corpus/templates/analytics/docs/error-capture.md +172 -0
  133. package/corpus/templates/analytics/docs/uptime-monitoring.md +150 -0
  134. package/corpus/templates/analytics/server/db/schema-errors.ts +150 -0
  135. package/corpus/templates/analytics/server/db/schema-monitoring.ts +177 -0
  136. package/corpus/templates/analytics/server/db/schema.ts +6 -0
  137. package/corpus/templates/analytics/server/jobs/uptime-monitors.ts +123 -0
  138. package/corpus/templates/analytics/server/lib/agent-chat-plan-mode.ts +41 -0
  139. package/corpus/templates/analytics/server/lib/data-dictionary-context.ts +55 -12
  140. package/corpus/templates/analytics/server/lib/error-capture.ts +1331 -0
  141. package/corpus/templates/analytics/server/lib/first-party-analytics.ts +47 -1
  142. package/corpus/templates/analytics/server/lib/monitor-stats.ts +494 -0
  143. package/corpus/templates/analytics/server/lib/status-pages.ts +798 -0
  144. package/corpus/templates/analytics/server/lib/uptime-monitors.ts +1665 -0
  145. package/corpus/templates/analytics/server/plugins/agent-chat.ts +7 -37
  146. package/corpus/templates/analytics/server/plugins/auth.ts +11 -1
  147. package/corpus/templates/analytics/server/plugins/db.ts +408 -0
  148. package/corpus/templates/analytics/server/plugins/uptime-monitor-jobs.ts +63 -0
  149. package/corpus/templates/assets/app/routes/_index.tsx +1 -1
  150. package/corpus/templates/assets/app/routes/settings.tsx +22 -1
  151. package/corpus/templates/assets/changelog/2026-07-08-recent-drafts-keep-comfortable-side-padding.md +6 -0
  152. package/corpus/templates/assets/changelog/2026-07-08-settings-are-cleaner-and-searchable.md +5 -0
  153. package/corpus/templates/brain/app/routes/settings.tsx +43 -5
  154. package/corpus/templates/brain/changelog/2026-07-08-settings-are-cleaner-and-searchable.md +5 -0
  155. package/corpus/templates/calendar/app/pages/Settings.tsx +44 -6
  156. package/corpus/templates/calendar/changelog/2026-07-08-settings-are-cleaner-and-searchable.md +5 -0
  157. package/corpus/templates/chat/app/routes/settings.tsx +16 -1
  158. package/corpus/templates/chat/changelog/2026-07-08-settings-are-cleaner-and-searchable.md +5 -0
  159. package/corpus/templates/clips/actions/lib/audio-only-transcription.ts +79 -37
  160. package/corpus/templates/clips/app/components/player/player-controls.tsx +35 -0
  161. package/corpus/templates/clips/app/components/player/video-player.tsx +162 -18
  162. package/corpus/templates/clips/app/routes/_app.settings._index.tsx +63 -6
  163. package/corpus/templates/clips/app/routes/record.tsx +18 -1
  164. package/corpus/templates/clips/changelog/2026-07-07-shared-clips-start-playback-reliably-from-the-first-play-click.md +6 -0
  165. package/corpus/templates/clips/changelog/2026-07-08-chrome-extension-camera-and-microphone-menus-now-show-which-.md +6 -0
  166. package/corpus/templates/clips/changelog/2026-07-08-chrome-extension-copies-new-clip-links-after-saving.md +6 -0
  167. package/corpus/templates/clips/changelog/2026-07-08-chrome-extension-recordings-now-continue-after-granting-came.md +6 -0
  168. package/corpus/templates/clips/changelog/2026-07-08-mobile-shared-clips-now-have-loom-style-tap-and-skip-controls.md +6 -0
  169. package/corpus/templates/clips/changelog/2026-07-08-recordings-and-uploads-copy-links-after-saving.md +6 -0
  170. package/corpus/templates/clips/changelog/2026-07-08-settings-are-cleaner-searchable-and-collapse-s3-with-builder.md +5 -0
  171. package/corpus/templates/clips/changelog/2026-07-08-transcripts-are-more-reliable-for-longer-desktop-recordings-.md +6 -0
  172. package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -0
  173. package/corpus/templates/clips/chrome-extension/src/background.ts +162 -13
  174. package/corpus/templates/clips/chrome-extension/src/content-script.ts +14 -0
  175. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +36 -0
  176. package/corpus/templates/clips/chrome-extension/src/overlay.ts +99 -25
  177. package/corpus/templates/clips/chrome-extension/src/permission.ts +63 -14
  178. package/corpus/templates/clips/chrome-extension/src/popup.ts +123 -23
  179. package/corpus/templates/content/actions/_builder-cms-read-client.ts +47 -0
  180. package/corpus/templates/content/actions/_database-source-utils.ts +33 -4
  181. package/corpus/templates/content/actions/add-content-database-source-field-property.ts +214 -29
  182. package/corpus/templates/content/actions/attach-content-database-source.ts +3 -0
  183. package/corpus/templates/content/actions/change-content-database-source-role.ts +1 -0
  184. package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +42 -32
  185. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +352 -81
  186. package/corpus/templates/content/app/components/editor/database/settings.tsx +49 -24
  187. package/corpus/templates/content/app/routes/_app.settings.tsx +19 -1
  188. package/corpus/templates/content/changelog/2026-07-07-builder-source-tag-fields-now-import-as-multi-select.md +6 -0
  189. package/corpus/templates/content/changelog/2026-07-08-settings-are-cleaner-and-searchable.md +5 -0
  190. package/corpus/templates/content/shared/api.ts +3 -0
  191. package/corpus/templates/content/shared/properties.ts +42 -0
  192. package/corpus/templates/design/actions/generate-design.ts +3 -0
  193. package/corpus/templates/design/actions/get-design-system.ts +147 -1
  194. package/corpus/templates/design/app/routes/settings.tsx +16 -1
  195. package/corpus/templates/design/changelog/2026-07-08-design-generation-now-follows-hydrated-builder-design-system.md +6 -0
  196. package/corpus/templates/design/changelog/2026-07-08-settings-are-cleaner-and-searchable.md +5 -0
  197. package/corpus/templates/dispatch/app/routes/settings.tsx +23 -2
  198. package/corpus/templates/dispatch/changelog/2026-07-08-settings-are-cleaner-and-searchable.md +5 -0
  199. package/corpus/templates/forms/app/routes/_app.settings.tsx +16 -1
  200. package/corpus/templates/forms/changelog/2026-07-08-settings-are-cleaner-and-searchable.md +5 -0
  201. package/corpus/templates/macros/app/routes/settings.tsx +16 -1
  202. package/corpus/templates/macros/changelog/2026-07-08-settings-are-cleaner-and-searchable.md +5 -0
  203. package/corpus/templates/mail/app/components/settings/GmailFiltersSection.tsx +1 -1
  204. package/corpus/templates/mail/app/components/settings/SnippetsSection.tsx +1 -1
  205. package/corpus/templates/mail/app/pages/SettingsPage.tsx +123 -105
  206. package/corpus/templates/mail/changelog/2026-07-08-settings-redesigned-with-a-consistent-searchable-layout.md +5 -0
  207. package/corpus/templates/plan/app/routes/settings.tsx +23 -2
  208. package/corpus/templates/plan/changelog/2026-07-08-settings-are-cleaner-and-searchable.md +5 -0
  209. package/corpus/templates/slides/actions/add-slide.ts +1 -0
  210. package/corpus/templates/slides/actions/get-design-system.ts +147 -1
  211. package/corpus/templates/slides/app/pages/Index.tsx +46 -1
  212. package/corpus/templates/slides/app/routes/settings.tsx +16 -1
  213. package/corpus/templates/slides/changelog/2026-07-08-deck-generation-now-follows-hydrated-builder-design-system-g.md +6 -0
  214. package/corpus/templates/slides/changelog/2026-07-08-settings-are-cleaner-and-searchable.md +5 -0
  215. package/dist/agent/run-ownership.d.ts +6 -0
  216. package/dist/agent/run-ownership.d.ts.map +1 -1
  217. package/dist/agent/run-ownership.js +11 -1
  218. package/dist/agent/run-ownership.js.map +1 -1
  219. package/dist/changelog/parse.d.ts +7 -0
  220. package/dist/changelog/parse.d.ts.map +1 -1
  221. package/dist/changelog/parse.js +64 -0
  222. package/dist/changelog/parse.js.map +1 -1
  223. package/dist/chat-threads/schema.d.ts +445 -0
  224. package/dist/chat-threads/schema.d.ts.map +1 -0
  225. package/dist/chat-threads/schema.js +39 -0
  226. package/dist/chat-threads/schema.js.map +1 -0
  227. package/dist/chat-threads/store.d.ts +12 -0
  228. package/dist/chat-threads/store.d.ts.map +1 -1
  229. package/dist/chat-threads/store.js +99 -10
  230. package/dist/chat-threads/store.js.map +1 -1
  231. package/dist/cli/workspace-dev.d.ts.map +1 -1
  232. package/dist/cli/workspace-dev.js +20 -5
  233. package/dist/cli/workspace-dev.js.map +1 -1
  234. package/dist/client/AgentPanel.d.ts.map +1 -1
  235. package/dist/client/AgentPanel.js +35 -7
  236. package/dist/client/AgentPanel.js.map +1 -1
  237. package/dist/client/AssistantChat.d.ts.map +1 -1
  238. package/dist/client/AssistantChat.js +105 -183
  239. package/dist/client/AssistantChat.js.map +1 -1
  240. package/dist/client/analytics-session.d.ts +3 -0
  241. package/dist/client/analytics-session.d.ts.map +1 -0
  242. package/dist/client/analytics-session.js +66 -0
  243. package/dist/client/analytics-session.js.map +1 -0
  244. package/dist/client/analytics.d.ts +32 -0
  245. package/dist/client/analytics.d.ts.map +1 -1
  246. package/dist/client/analytics.js +116 -46
  247. package/dist/client/analytics.js.map +1 -1
  248. package/dist/client/chat/message-components.d.ts +2 -1
  249. package/dist/client/chat/message-components.d.ts.map +1 -1
  250. package/dist/client/chat/message-components.js +16 -6
  251. package/dist/client/chat/message-components.js.map +1 -1
  252. package/dist/client/components/ui/message-scroller.d.ts +16 -0
  253. package/dist/client/components/ui/message-scroller.d.ts.map +1 -0
  254. package/dist/client/components/ui/message-scroller.js +25 -0
  255. package/dist/client/components/ui/message-scroller.js.map +1 -0
  256. package/dist/client/conversation/AgentConversation.d.ts +1 -1
  257. package/dist/client/conversation/AgentConversation.d.ts.map +1 -1
  258. package/dist/client/conversation/AgentConversation.js +3 -8
  259. package/dist/client/conversation/AgentConversation.js.map +1 -1
  260. package/dist/client/error-capture.d.ts +99 -0
  261. package/dist/client/error-capture.d.ts.map +1 -0
  262. package/dist/client/error-capture.js +437 -0
  263. package/dist/client/error-capture.js.map +1 -0
  264. package/dist/client/index.d.ts +2 -2
  265. package/dist/client/index.d.ts.map +1 -1
  266. package/dist/client/index.js +3 -1
  267. package/dist/client/index.js.map +1 -1
  268. package/dist/client/session-replay.d.ts +22 -0
  269. package/dist/client/session-replay.d.ts.map +1 -1
  270. package/dist/client/session-replay.js +45 -4
  271. package/dist/client/session-replay.js.map +1 -1
  272. package/dist/client/settings/SettingsPanel.d.ts.map +1 -1
  273. package/dist/client/settings/SettingsPanel.js +100 -5
  274. package/dist/client/settings/SettingsPanel.js.map +1 -1
  275. package/dist/client/settings/SettingsTabsPage.d.ts +49 -1
  276. package/dist/client/settings/SettingsTabsPage.d.ts.map +1 -1
  277. package/dist/client/settings/SettingsTabsPage.js +131 -19
  278. package/dist/client/settings/SettingsTabsPage.js.map +1 -1
  279. package/dist/client/settings/index.d.ts +1 -1
  280. package/dist/client/settings/index.d.ts.map +1 -1
  281. package/dist/client/settings/index.js.map +1 -1
  282. package/dist/client/sharing/ShareButton.d.ts.map +1 -1
  283. package/dist/client/sharing/ShareButton.js +13 -8
  284. package/dist/client/sharing/ShareButton.js.map +1 -1
  285. package/dist/client/sharing/ShareDialog.d.ts.map +1 -1
  286. package/dist/client/sharing/ShareDialog.js +12 -7
  287. package/dist/client/sharing/ShareDialog.js.map +1 -1
  288. package/dist/collab/struct-routes.d.ts +1 -1
  289. package/dist/mcp/builtin-tools.d.ts.map +1 -1
  290. package/dist/mcp/builtin-tools.js +21 -2
  291. package/dist/mcp/builtin-tools.js.map +1 -1
  292. package/dist/notifications/routes.d.ts +1 -1
  293. package/dist/org/schema.d.ts +15 -15
  294. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  295. package/dist/resources/handlers.d.ts +3 -3
  296. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  297. package/dist/server/agent-chat-plugin.js +109 -73
  298. package/dist/server/agent-chat-plugin.js.map +1 -1
  299. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  300. package/dist/server/onboarding-html.d.ts.map +1 -1
  301. package/dist/server/onboarding-html.js +28 -16
  302. package/dist/server/onboarding-html.js.map +1 -1
  303. package/dist/sharing/actions/list-resource-shares.js +30 -1
  304. package/dist/sharing/actions/list-resource-shares.js.map +1 -1
  305. package/dist/sharing/actions/share-resource.js +6 -0
  306. package/dist/sharing/actions/share-resource.js.map +1 -1
  307. package/dist/templates/workspace-core/.agents/skills/agent-native-toolkit/SKILL.md +4 -2
  308. package/dist/templates/workspace-core/.agents/skills/changelog/SKILL.md +9 -6
  309. package/dist/vite/client.d.ts.map +1 -1
  310. package/dist/vite/client.js +192 -30
  311. package/dist/vite/client.js.map +1 -1
  312. package/docs/content/toolkit-settings.mdx +56 -3
  313. package/package.json +2 -1
  314. package/src/templates/workspace-core/.agents/skills/agent-native-toolkit/SKILL.md +4 -2
  315. package/src/templates/workspace-core/.agents/skills/changelog/SKILL.md +9 -6
@@ -0,0 +1,798 @@
1
+ /**
2
+ * Public status pages.
3
+ *
4
+ * A status page bundles a set of the owner's uptime monitors under a public
5
+ * `slug` and renders their SAFE aggregate health to anyone with the link — but
6
+ * only once the owner publishes it.
7
+ *
8
+ * Security model (see the `security` and `sharing` skills):
9
+ * - CRUD (`listStatusPages` / `getStatusPage` / `saveStatusPage` /
10
+ * `deleteStatusPage` + monitor management) is owner-scoped by
11
+ * owner_email + org_id, mirroring server/lib/uptime-monitors.ts. Monitors
12
+ * added to a page are verified to belong to the caller.
13
+ * - `getPublicStatusPage(slug)` is the ONLY unauthenticated read. It returns
14
+ * a page strictly when `published` is true, resolves the included monitors
15
+ * scoped to THE PAGE OWNER (never the anonymous requester), and returns
16
+ * only a sanitized projection — display name / host, current status, uptime
17
+ * percentages, timeline buckets, and optional response time. It NEVER leaks
18
+ * the monitor URL/headers/body/assertions/expected-status/alert channels,
19
+ * and only surfaces the full URL for a monitor whose per-page "show URL"
20
+ * opt-in is set.
21
+ * - The pure helpers (`normalizeSlug`, `parseStatusPageMonitors`,
22
+ * `sanitizePublicMonitor`, `computeOverallStatus`, `aggregateWindows`) are
23
+ * unit-tested (status-pages.spec.ts).
24
+ */
25
+ import { randomUUID } from "node:crypto";
26
+
27
+ import { recordChange } from "@agent-native/core/server";
28
+ import { and, desc, eq, inArray, isNull, sql } from "drizzle-orm";
29
+
30
+ import { getDb, schema } from "../db/index.js";
31
+ import {
32
+ getMonitorStats,
33
+ type MonitorStats,
34
+ type ResponseTimePoint,
35
+ type UptimeBucket,
36
+ type UptimeWindows,
37
+ } from "./monitor-stats.js";
38
+ import { type AccessCtx, type MonitorStatus } from "./uptime-monitors.js";
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // Types
42
+ // ---------------------------------------------------------------------------
43
+
44
+ export type StatusPageDensity = "comfortable" | "compact";
45
+ export type StatusPageAlignment = "left" | "center";
46
+
47
+ export interface StatusPageMonitorRef {
48
+ monitorId: string;
49
+ order: number;
50
+ displayName: string | null;
51
+ showUrl: boolean;
52
+ }
53
+
54
+ export interface StatusPage {
55
+ id: string;
56
+ slug: string;
57
+ title: string;
58
+ description: string | null;
59
+ published: boolean;
60
+ showUptimeBars: boolean;
61
+ showOverallUptime: boolean;
62
+ showResponseTime: boolean;
63
+ density: StatusPageDensity;
64
+ alignment: StatusPageAlignment;
65
+ monitors: StatusPageMonitorRef[];
66
+ createdAt: string;
67
+ updatedAt: string;
68
+ ownerEmail: string;
69
+ orgId: string | null;
70
+ }
71
+
72
+ export interface StatusPageMonitorInput {
73
+ monitorId: string;
74
+ displayName?: string | null;
75
+ showUrl?: boolean;
76
+ }
77
+
78
+ export interface StatusPageInput {
79
+ id?: string;
80
+ slug?: string;
81
+ title?: string;
82
+ description?: string | null;
83
+ published?: boolean;
84
+ showUptimeBars?: boolean;
85
+ showOverallUptime?: boolean;
86
+ showResponseTime?: boolean;
87
+ density?: StatusPageDensity;
88
+ alignment?: StatusPageAlignment;
89
+ monitors?: StatusPageMonitorInput[];
90
+ }
91
+
92
+ export type OverallStatus = "operational" | "degraded" | "down" | "unknown";
93
+
94
+ /** Sanitized, public-safe monitor projection. NO sensitive config fields. */
95
+ export interface PublicStatusMonitor {
96
+ id: string;
97
+ name: string;
98
+ /** Host of the monitored URL — safe to show; the full URL is gated by showUrl. */
99
+ host: string | null;
100
+ /** Full URL, only present when the owner enabled "show URL" for this monitor. */
101
+ url: string | null;
102
+ status: MonitorStatus | null;
103
+ windows: UptimeWindows;
104
+ timeline: UptimeBucket[];
105
+ responseSeries: ResponseTimePoint[];
106
+ avgResponseMs: number | null;
107
+ }
108
+
109
+ export interface PublicStatusPage {
110
+ slug: string;
111
+ title: string;
112
+ description: string | null;
113
+ layout: {
114
+ density: StatusPageDensity;
115
+ alignment: StatusPageAlignment;
116
+ showUptimeBars: boolean;
117
+ showOverallUptime: boolean;
118
+ showResponseTime: boolean;
119
+ };
120
+ overall: OverallStatus;
121
+ counts: { up: number; down: number; degraded: number; total: number };
122
+ overallWindows: UptimeWindows;
123
+ monitors: PublicStatusMonitor[];
124
+ generatedAt: string;
125
+ }
126
+
127
+ // ---------------------------------------------------------------------------
128
+ // Constants
129
+ // ---------------------------------------------------------------------------
130
+
131
+ const MAX_STATUS_PAGES_PER_OWNER = 50;
132
+ const MAX_MONITORS_PER_PAGE = 50;
133
+ const MAX_TITLE_LENGTH = 120;
134
+ const MAX_DESCRIPTION_LENGTH = 500;
135
+ const MAX_DISPLAY_NAME_LENGTH = 120;
136
+ const MAX_SLUG_LENGTH = 64;
137
+ const TIMELINE_DAYS = 90;
138
+ const RESPONSE_WINDOW_HOURS_WITH_CHART = 24 * 7;
139
+
140
+ const EMPTY_WINDOWS: UptimeWindows = {
141
+ uptime24h: null,
142
+ uptime7d: null,
143
+ uptime30d: null,
144
+ uptime90d: null,
145
+ };
146
+
147
+ // ---------------------------------------------------------------------------
148
+ // Small helpers
149
+ // ---------------------------------------------------------------------------
150
+
151
+ function nowIso(): string {
152
+ return new Date().toISOString();
153
+ }
154
+
155
+ function badRequest(message: string): Error {
156
+ return Object.assign(new Error(message), { statusCode: 400 });
157
+ }
158
+
159
+ function notFound(message: string): Error {
160
+ return Object.assign(new Error(message), { statusCode: 404 });
161
+ }
162
+
163
+ function safeJsonParse<T>(raw: unknown, fallback: T): T {
164
+ if (typeof raw !== "string" || !raw.trim()) return fallback;
165
+ try {
166
+ const parsed = JSON.parse(raw) as T;
167
+ return parsed ?? fallback;
168
+ } catch {
169
+ return fallback;
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Strict host extraction for the public boundary: returns ONLY the host of a
175
+ * parseable http(s) URL, never the raw string. If the value can't be parsed we
176
+ * return null rather than risk leaking a full URL/path/query to the public.
177
+ */
178
+ function safeHost(url: unknown): string | null {
179
+ if (typeof url !== "string" || !url.trim()) return null;
180
+ try {
181
+ const parsed = new URL(url);
182
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
183
+ return null;
184
+ return parsed.host || null;
185
+ } catch {
186
+ return null;
187
+ }
188
+ }
189
+
190
+ /** Coarse tone for a monitor status (server-local; avoids importing client utils). */
191
+ function statusToTone(
192
+ status: MonitorStatus | null,
193
+ ): "up" | "down" | "degraded" | "neutral" {
194
+ switch (status) {
195
+ case "up":
196
+ return "up";
197
+ case "down":
198
+ case "error":
199
+ return "down";
200
+ case "degraded":
201
+ return "degraded";
202
+ default:
203
+ return "neutral";
204
+ }
205
+ }
206
+
207
+ // ---------------------------------------------------------------------------
208
+ // Pure normalization + sanitization (unit-tested)
209
+ // ---------------------------------------------------------------------------
210
+
211
+ /** URL-safe slug: lowercase, `[a-z0-9-]`, collapsed/trimmed dashes. */
212
+ export function normalizeSlug(raw: unknown): string {
213
+ return String(raw ?? "")
214
+ .toLowerCase()
215
+ .trim()
216
+ .replace(/[^a-z0-9]+/g, "-")
217
+ .replace(/^-+|-+$/g, "")
218
+ .slice(0, MAX_SLUG_LENGTH)
219
+ .replace(/^-+|-+$/g, "");
220
+ }
221
+
222
+ function normalizeDensity(value: unknown): StatusPageDensity {
223
+ return value === "compact" ? "compact" : "comfortable";
224
+ }
225
+
226
+ function normalizeAlignment(value: unknown): StatusPageAlignment {
227
+ return value === "center" ? "center" : "left";
228
+ }
229
+
230
+ function normalizeDisplayName(value: unknown): string | null {
231
+ if (value == null) return null;
232
+ const trimmed = String(value).trim();
233
+ if (!trimmed) return null;
234
+ return trimmed.slice(0, MAX_DISPLAY_NAME_LENGTH);
235
+ }
236
+
237
+ /**
238
+ * Parse the stored `monitors` JSON into ordered, de-duplicated refs. Invalid
239
+ * entries are dropped; `order` is assigned by position so callers never depend
240
+ * on the stored order field being trustworthy.
241
+ */
242
+ export function parseStatusPageMonitors(raw: unknown): StatusPageMonitorRef[] {
243
+ const arr = safeJsonParse<unknown[]>(raw, []);
244
+ if (!Array.isArray(arr)) return [];
245
+ const seen = new Set<string>();
246
+ const refs: StatusPageMonitorRef[] = [];
247
+ for (const entry of arr) {
248
+ if (!entry || typeof entry !== "object") continue;
249
+ const record = entry as Record<string, unknown>;
250
+ const monitorId = String(record.monitorId ?? "").trim();
251
+ if (!monitorId || seen.has(monitorId)) continue;
252
+ seen.add(monitorId);
253
+ refs.push({
254
+ monitorId,
255
+ order: refs.length,
256
+ displayName: normalizeDisplayName(record.displayName),
257
+ showUrl: record.showUrl === true,
258
+ });
259
+ if (refs.length >= MAX_MONITORS_PER_PAGE) break;
260
+ }
261
+ return refs;
262
+ }
263
+
264
+ function serializeMonitorRefs(refs: StatusPageMonitorRef[]): string {
265
+ return JSON.stringify(
266
+ refs.map((ref, index) => ({
267
+ monitorId: ref.monitorId,
268
+ order: index,
269
+ displayName: ref.displayName,
270
+ showUrl: ref.showUrl,
271
+ })),
272
+ );
273
+ }
274
+
275
+ /**
276
+ * Build the public-safe monitor projection. This is the security boundary for
277
+ * the anonymous read: it constructs a fresh object with ONLY safe fields and
278
+ * never spreads the raw monitor row, so config/secret fields cannot leak.
279
+ */
280
+ export function sanitizePublicMonitor(
281
+ monitor: {
282
+ id: string;
283
+ name: string;
284
+ url: string;
285
+ lastStatus: MonitorStatus | null;
286
+ },
287
+ ref: StatusPageMonitorRef,
288
+ stats: MonitorStats | undefined,
289
+ opts: { showResponseTime: boolean },
290
+ ): PublicStatusMonitor {
291
+ return {
292
+ id: monitor.id,
293
+ name: ref.displayName?.trim() || monitor.name,
294
+ host: safeHost(monitor.url),
295
+ url: ref.showUrl ? monitor.url : null,
296
+ status: stats?.status ?? monitor.lastStatus ?? null,
297
+ windows: stats?.windows ?? EMPTY_WINDOWS,
298
+ timeline: stats?.timeline ?? [],
299
+ responseSeries: opts.showResponseTime ? (stats?.responseSeries ?? []) : [],
300
+ avgResponseMs: opts.showResponseTime
301
+ ? (stats?.avgResponseMs ?? null)
302
+ : null,
303
+ };
304
+ }
305
+
306
+ /** Raw (owner-scoped) monitor row shape needed to build the public projection. */
307
+ export interface PublicMonitorRow {
308
+ id: string;
309
+ name: string;
310
+ url: string;
311
+ lastStatus: MonitorStatus | null;
312
+ }
313
+
314
+ /**
315
+ * Assemble the sanitized public monitor list from the page's ordered refs, the
316
+ * OWNER-SCOPED monitor rows, and the stats map. This is the exclusion boundary:
317
+ * a ref whose monitor row is absent (not owned by the page owner, deleted, or
318
+ * otherwise not returned by the owner-scoped query) is DROPPED — so a page can
319
+ * never surface a monitor that isn't both included AND owned. Pure + unit-tested.
320
+ */
321
+ export function assemblePublicMonitors(
322
+ refs: StatusPageMonitorRef[],
323
+ monitorRows: PublicMonitorRow[],
324
+ stats: Map<string, MonitorStats>,
325
+ opts: { showResponseTime: boolean },
326
+ ): PublicStatusMonitor[] {
327
+ const monitorById = new Map<string, PublicMonitorRow>(
328
+ monitorRows.map((row) => [row.id, row]),
329
+ );
330
+ return refs
331
+ .map((ref) => {
332
+ const monitor = monitorById.get(ref.monitorId);
333
+ if (!monitor) return null;
334
+ return sanitizePublicMonitor(monitor, ref, stats.get(ref.monitorId), {
335
+ showResponseTime: opts.showResponseTime,
336
+ });
337
+ })
338
+ .filter((monitor): monitor is PublicStatusMonitor => monitor != null);
339
+ }
340
+
341
+ export function computeOverallStatus(
342
+ monitors: Pick<PublicStatusMonitor, "status">[],
343
+ ): OverallStatus {
344
+ if (monitors.length === 0) return "unknown";
345
+ let down = 0;
346
+ let degraded = 0;
347
+ let up = 0;
348
+ for (const monitor of monitors) {
349
+ const tone = statusToTone(monitor.status);
350
+ if (tone === "down") down++;
351
+ else if (tone === "degraded") degraded++;
352
+ else if (tone === "up") up++;
353
+ }
354
+ if (down > 0) return "down";
355
+ if (degraded > 0) return "degraded";
356
+ if (up > 0) return "operational";
357
+ return "unknown";
358
+ }
359
+
360
+ function computeCounts(monitors: Pick<PublicStatusMonitor, "status">[]) {
361
+ const counts = { up: 0, down: 0, degraded: 0, total: monitors.length };
362
+ for (const monitor of monitors) {
363
+ const tone = statusToTone(monitor.status);
364
+ if (tone === "up") counts.up++;
365
+ else if (tone === "down") counts.down++;
366
+ else if (tone === "degraded") counts.degraded++;
367
+ }
368
+ return counts;
369
+ }
370
+
371
+ /** Overall uptime per window = mean of the monitors that have data. */
372
+ export function aggregateWindows(windowsList: UptimeWindows[]): UptimeWindows {
373
+ const mean = (key: keyof UptimeWindows): number | null => {
374
+ const values = windowsList
375
+ .map((w) => w[key])
376
+ .filter((v): v is number => v != null);
377
+ if (values.length === 0) return null;
378
+ return values.reduce((sum, v) => sum + v, 0) / values.length;
379
+ };
380
+ return {
381
+ uptime24h: mean("uptime24h"),
382
+ uptime7d: mean("uptime7d"),
383
+ uptime30d: mean("uptime30d"),
384
+ uptime90d: mean("uptime90d"),
385
+ };
386
+ }
387
+
388
+ // ---------------------------------------------------------------------------
389
+ // Row mapping + owner scoping
390
+ // ---------------------------------------------------------------------------
391
+
392
+ function rowToStatusPage(row: any): StatusPage {
393
+ return {
394
+ id: row.id,
395
+ slug: row.slug,
396
+ title: row.title,
397
+ description: row.description ?? null,
398
+ published: row.published === true || row.published === 1,
399
+ showUptimeBars: row.showUptimeBars === true || row.showUptimeBars === 1,
400
+ showOverallUptime:
401
+ row.showOverallUptime === true || row.showOverallUptime === 1,
402
+ showResponseTime:
403
+ row.showResponseTime === true || row.showResponseTime === 1,
404
+ density: normalizeDensity(row.density),
405
+ alignment: normalizeAlignment(row.alignment),
406
+ monitors: parseStatusPageMonitors(row.monitors),
407
+ createdAt: row.createdAt,
408
+ updatedAt: row.updatedAt,
409
+ ownerEmail: row.ownerEmail,
410
+ orgId: row.orgId ?? null,
411
+ };
412
+ }
413
+
414
+ function pageOwnerWhere(ctx: AccessCtx, id?: string) {
415
+ const table = schema.statusPages;
416
+ const clauses = [
417
+ sql`lower(${table.ownerEmail}) = ${ctx.email.toLowerCase()}`,
418
+ ctx.orgId ? eq(table.orgId, ctx.orgId) : isNull(table.orgId),
419
+ ];
420
+ if (id) clauses.push(eq(table.id, id));
421
+ return and(...clauses);
422
+ }
423
+
424
+ function monitorsOwnerWhere(ctx: AccessCtx) {
425
+ const table = schema.monitors;
426
+ return and(
427
+ sql`lower(${table.ownerEmail}) = ${ctx.email.toLowerCase()}`,
428
+ ctx.orgId ? eq(table.orgId, ctx.orgId) : isNull(table.orgId),
429
+ );
430
+ }
431
+
432
+ /** Keep only monitor refs the caller actually owns; re-number order. */
433
+ async function resolveOwnedMonitorRefs(
434
+ ctx: AccessCtx,
435
+ entries: StatusPageMonitorInput[],
436
+ ): Promise<StatusPageMonitorRef[]> {
437
+ const seen = new Set<string>();
438
+ const requested: StatusPageMonitorRef[] = [];
439
+ for (const entry of entries) {
440
+ const monitorId = String(entry?.monitorId ?? "").trim();
441
+ if (!monitorId || seen.has(monitorId)) continue;
442
+ seen.add(monitorId);
443
+ requested.push({
444
+ monitorId,
445
+ order: requested.length,
446
+ displayName: normalizeDisplayName(entry.displayName),
447
+ showUrl: entry.showUrl === true,
448
+ });
449
+ if (requested.length >= MAX_MONITORS_PER_PAGE) break;
450
+ }
451
+ if (requested.length === 0) return [];
452
+
453
+ const db = getDb() as any;
454
+ const ownedRows = await db
455
+ .select({ id: schema.monitors.id })
456
+ .from(schema.monitors)
457
+ .where(
458
+ and(
459
+ monitorsOwnerWhere(ctx),
460
+ inArray(
461
+ schema.monitors.id,
462
+ requested.map((ref) => ref.monitorId),
463
+ ),
464
+ ),
465
+ );
466
+ const owned = new Set<string>(ownedRows.map((row: any) => row.id));
467
+ return requested
468
+ .filter((ref) => owned.has(ref.monitorId))
469
+ .map((ref, index) => ({ ...ref, order: index }));
470
+ }
471
+
472
+ async function slugIsTaken(slug: string, exceptId?: string): Promise<boolean> {
473
+ const db = getDb() as any;
474
+ // Slug is a GLOBAL public namespace (`/status/<slug>`), so uniqueness is
475
+ // checked across all owners intentionally — not owner-scoped.
476
+ // guard:allow-unscoped — global slug uniqueness check for the public URL namespace
477
+ const rows = await db
478
+ .select({ id: schema.statusPages.id })
479
+ .from(schema.statusPages)
480
+ .where(eq(schema.statusPages.slug, slug))
481
+ .limit(2);
482
+ return rows.some((row: any) => row.id !== exceptId);
483
+ }
484
+
485
+ // ---------------------------------------------------------------------------
486
+ // Owner-scoped CRUD
487
+ // ---------------------------------------------------------------------------
488
+
489
+ export async function listStatusPages(ctx: AccessCtx): Promise<StatusPage[]> {
490
+ const db = getDb() as any;
491
+ const rows = await db
492
+ .select()
493
+ .from(schema.statusPages)
494
+ .where(pageOwnerWhere(ctx))
495
+ .orderBy(desc(schema.statusPages.updatedAt));
496
+ return rows.map(rowToStatusPage);
497
+ }
498
+
499
+ export async function getStatusPage(
500
+ id: string,
501
+ ctx: AccessCtx,
502
+ ): Promise<StatusPage | null> {
503
+ const db = getDb() as any;
504
+ const [row] = await db
505
+ .select()
506
+ .from(schema.statusPages)
507
+ .where(pageOwnerWhere(ctx, id))
508
+ .limit(1);
509
+ return row ? rowToStatusPage(row) : null;
510
+ }
511
+
512
+ export async function saveStatusPage(
513
+ input: StatusPageInput,
514
+ ctx: AccessCtx,
515
+ ): Promise<StatusPage> {
516
+ const db = getDb() as any;
517
+ const updatedAt = nowIso();
518
+ const id = input.id || randomUUID();
519
+
520
+ const existing = input.id ? await getStatusPage(input.id, ctx) : null;
521
+ if (input.id && !existing) throw notFound("Status page not found");
522
+
523
+ const title = (input.title ?? existing?.title ?? "").trim();
524
+ if (!title) throw badRequest("Status page title is required");
525
+
526
+ // Slug: normalize the provided value, else keep the existing one, else derive
527
+ // it from the title (with a short random suffix as a last resort).
528
+ let slug =
529
+ input.slug != null ? normalizeSlug(input.slug) : (existing?.slug ?? "");
530
+ if (!slug) slug = normalizeSlug(title);
531
+ if (!slug) slug = `status-${randomUUID().slice(0, 8)}`;
532
+ if (await slugIsTaken(slug, existing?.id)) {
533
+ throw badRequest(`The slug "${slug}" is already in use`);
534
+ }
535
+
536
+ const description =
537
+ input.description !== undefined
538
+ ? input.description
539
+ ? String(input.description).trim().slice(0, MAX_DESCRIPTION_LENGTH)
540
+ : null
541
+ : (existing?.description ?? null);
542
+
543
+ const refs =
544
+ input.monitors !== undefined
545
+ ? await resolveOwnedMonitorRefs(ctx, input.monitors)
546
+ : (existing?.monitors ?? []);
547
+
548
+ const shared = {
549
+ slug,
550
+ title: title.slice(0, MAX_TITLE_LENGTH),
551
+ description,
552
+ published: input.published ?? existing?.published ?? false,
553
+ showUptimeBars: input.showUptimeBars ?? existing?.showUptimeBars ?? true,
554
+ showOverallUptime:
555
+ input.showOverallUptime ?? existing?.showOverallUptime ?? true,
556
+ showResponseTime:
557
+ input.showResponseTime ?? existing?.showResponseTime ?? false,
558
+ density: normalizeDensity(input.density ?? existing?.density),
559
+ alignment: normalizeAlignment(input.alignment ?? existing?.alignment),
560
+ monitors: serializeMonitorRefs(refs),
561
+ };
562
+
563
+ if (existing) {
564
+ await db
565
+ .update(schema.statusPages)
566
+ .set({ ...shared, updatedAt })
567
+ .where(pageOwnerWhere(ctx, id));
568
+ } else {
569
+ const [{ total = 0 } = { total: 0 }] = await db
570
+ .select({ total: sql<number>`count(*)` })
571
+ .from(schema.statusPages)
572
+ .where(pageOwnerWhere(ctx));
573
+ if (Number(total) >= MAX_STATUS_PAGES_PER_OWNER) {
574
+ throw Object.assign(
575
+ new Error(`Status page limit reached (${MAX_STATUS_PAGES_PER_OWNER})`),
576
+ { statusCode: 429 },
577
+ );
578
+ }
579
+ await db.insert(schema.statusPages).values({
580
+ id,
581
+ ...shared,
582
+ createdAt: updatedAt,
583
+ updatedAt,
584
+ ownerEmail: ctx.email,
585
+ orgId: ctx.orgId,
586
+ });
587
+ }
588
+
589
+ const saved = await getStatusPage(id, ctx);
590
+ if (!saved) throw new Error("Failed to save status page");
591
+ recordChange({
592
+ source: "status-pages",
593
+ type: "change",
594
+ key: saved.id,
595
+ owner: saved.ownerEmail,
596
+ orgId: saved.orgId ?? undefined,
597
+ });
598
+ return saved;
599
+ }
600
+
601
+ export async function deleteStatusPage(
602
+ id: string,
603
+ ctx: AccessCtx,
604
+ ): Promise<void> {
605
+ const existing = await getStatusPage(id, ctx);
606
+ if (!existing) throw notFound("Status page not found");
607
+ const db = getDb() as any;
608
+ await db.delete(schema.statusPages).where(pageOwnerWhere(ctx, id));
609
+ recordChange({
610
+ source: "status-pages",
611
+ type: "delete",
612
+ key: id,
613
+ owner: existing.ownerEmail,
614
+ orgId: existing.orgId ?? undefined,
615
+ });
616
+ }
617
+
618
+ // ---------------------------------------------------------------------------
619
+ // Monitor membership management
620
+ // ---------------------------------------------------------------------------
621
+
622
+ async function persistMonitorRefs(
623
+ page: StatusPage,
624
+ refs: StatusPageMonitorRef[],
625
+ ctx: AccessCtx,
626
+ ): Promise<StatusPage> {
627
+ const db = getDb() as any;
628
+ const ordered = refs.map((ref, index) => ({ ...ref, order: index }));
629
+ await db
630
+ .update(schema.statusPages)
631
+ .set({ monitors: serializeMonitorRefs(ordered), updatedAt: nowIso() })
632
+ .where(pageOwnerWhere(ctx, page.id));
633
+ const saved = await getStatusPage(page.id, ctx);
634
+ if (!saved) throw new Error("Failed to update status page monitors");
635
+ recordChange({
636
+ source: "status-pages",
637
+ type: "change",
638
+ key: saved.id,
639
+ owner: saved.ownerEmail,
640
+ orgId: saved.orgId ?? undefined,
641
+ });
642
+ return saved;
643
+ }
644
+
645
+ export async function addStatusPageMonitor(
646
+ pageId: string,
647
+ input: StatusPageMonitorInput,
648
+ ctx: AccessCtx,
649
+ ): Promise<StatusPage> {
650
+ const page = await getStatusPage(pageId, ctx);
651
+ if (!page) throw notFound("Status page not found");
652
+ const [owned] = await resolveOwnedMonitorRefs(ctx, [input]);
653
+ if (!owned) throw badRequest("Monitor not found");
654
+ if (page.monitors.length >= MAX_MONITORS_PER_PAGE) {
655
+ throw badRequest(
656
+ `A status page can include up to ${MAX_MONITORS_PER_PAGE} monitors`,
657
+ );
658
+ }
659
+ const next = page.monitors.filter((ref) => ref.monitorId !== owned.monitorId);
660
+ next.push(owned);
661
+ return persistMonitorRefs(page, next, ctx);
662
+ }
663
+
664
+ export async function removeStatusPageMonitor(
665
+ pageId: string,
666
+ monitorId: string,
667
+ ctx: AccessCtx,
668
+ ): Promise<StatusPage> {
669
+ const page = await getStatusPage(pageId, ctx);
670
+ if (!page) throw notFound("Status page not found");
671
+ const next = page.monitors.filter((ref) => ref.monitorId !== monitorId);
672
+ return persistMonitorRefs(page, next, ctx);
673
+ }
674
+
675
+ export async function reorderStatusPageMonitors(
676
+ pageId: string,
677
+ orderedMonitorIds: string[],
678
+ ctx: AccessCtx,
679
+ ): Promise<StatusPage> {
680
+ const page = await getStatusPage(pageId, ctx);
681
+ if (!page) throw notFound("Status page not found");
682
+ const byId = new Map(page.monitors.map((ref) => [ref.monitorId, ref]));
683
+ const next: StatusPageMonitorRef[] = [];
684
+ for (const id of orderedMonitorIds) {
685
+ const ref = byId.get(id);
686
+ if (ref && !next.includes(ref)) next.push(ref);
687
+ }
688
+ // Append any monitors the caller didn't mention so none are silently dropped.
689
+ for (const ref of page.monitors) {
690
+ if (!next.includes(ref)) next.push(ref);
691
+ }
692
+ return persistMonitorRefs(page, next, ctx);
693
+ }
694
+
695
+ // ---------------------------------------------------------------------------
696
+ // View assembly (shared by public read + owner preview)
697
+ // ---------------------------------------------------------------------------
698
+
699
+ async function buildStatusPageView(
700
+ page: StatusPage,
701
+ ): Promise<PublicStatusPage> {
702
+ const ownerCtx: AccessCtx = { email: page.ownerEmail, orgId: page.orgId };
703
+ const layout = {
704
+ density: page.density,
705
+ alignment: page.alignment,
706
+ showUptimeBars: page.showUptimeBars,
707
+ showOverallUptime: page.showOverallUptime,
708
+ showResponseTime: page.showResponseTime,
709
+ };
710
+
711
+ let publicMonitors: PublicStatusMonitor[] = [];
712
+ if (page.monitors.length > 0) {
713
+ const db = getDb() as any;
714
+ const ids = page.monitors.map((ref) => ref.monitorId);
715
+ // Resolve the included monitors scoped strictly to the PAGE OWNER — an
716
+ // anonymous viewer never widens this beyond the owner's own monitors.
717
+ const monitorRows = await db
718
+ .select({
719
+ id: schema.monitors.id,
720
+ name: schema.monitors.name,
721
+ url: schema.monitors.url,
722
+ lastStatus: schema.monitors.lastStatus,
723
+ })
724
+ .from(schema.monitors)
725
+ .where(
726
+ and(monitorsOwnerWhere(ownerCtx), inArray(schema.monitors.id, ids)),
727
+ );
728
+ const ownedIds = new Set<string>(monitorRows.map((row: any) => row.id));
729
+ const ownedIncludedIds = page.monitors
730
+ .filter((ref) => ownedIds.has(ref.monitorId))
731
+ .map((ref) => ref.monitorId);
732
+ const stats = await getMonitorStats(ownerCtx, ownedIncludedIds, {
733
+ timelineDays: TIMELINE_DAYS,
734
+ responseWindowHours: page.showResponseTime
735
+ ? RESPONSE_WINDOW_HOURS_WITH_CHART
736
+ : 24,
737
+ });
738
+ publicMonitors = assemblePublicMonitors(
739
+ page.monitors,
740
+ monitorRows as PublicMonitorRow[],
741
+ stats,
742
+ { showResponseTime: page.showResponseTime },
743
+ );
744
+ }
745
+
746
+ return {
747
+ slug: page.slug,
748
+ title: page.title,
749
+ description: page.description,
750
+ layout,
751
+ overall: computeOverallStatus(publicMonitors),
752
+ counts: computeCounts(publicMonitors),
753
+ overallWindows: publicMonitors.length
754
+ ? aggregateWindows(publicMonitors.map((m) => m.windows))
755
+ : EMPTY_WINDOWS,
756
+ monitors: publicMonitors,
757
+ generatedAt: nowIso(),
758
+ };
759
+ }
760
+
761
+ /**
762
+ * UNAUTHENTICATED read by slug. Returns the sanitized public view only for a
763
+ * PUBLISHED page, or null (unknown/unpublished → 404 on the route).
764
+ */
765
+ export async function getPublicStatusPage(
766
+ slug: string,
767
+ ): Promise<PublicStatusPage | null> {
768
+ const normalized = normalizeSlug(slug);
769
+ if (!normalized) return null;
770
+ const db = getDb() as any;
771
+ const table = schema.statusPages;
772
+ // Public viewer endpoint: look up strictly by slug AND published=true. Not
773
+ // owner-scoped by design (there is no authenticated requester); the included
774
+ // monitors are re-scoped to the page owner inside buildStatusPageView.
775
+ // guard:allow-unscoped — public status page viewer; published-only, monitor reads re-scoped to page owner
776
+ const [row] = await db
777
+ .select()
778
+ .from(table)
779
+ .where(and(eq(table.slug, normalized), eq(table.published, true)))
780
+ .limit(1);
781
+ if (!row) return null;
782
+ return buildStatusPageView(rowToStatusPage(row));
783
+ }
784
+
785
+ /**
786
+ * Owner-scoped preview of the exact public view, for the in-app config UI so it
787
+ * can render the same output the public page will show (including unpublished
788
+ * drafts). Returns null when the page isn't owned by the caller.
789
+ */
790
+ export async function getStatusPagePreview(
791
+ id: string,
792
+ ctx: AccessCtx,
793
+ ): Promise<{ page: StatusPage; view: PublicStatusPage } | null> {
794
+ const page = await getStatusPage(id, ctx);
795
+ if (!page) return null;
796
+ const view = await buildStatusPageView(page);
797
+ return { page, view };
798
+ }