@storyshelf/app 0.6.2 → 0.7.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 (361) hide show
  1. package/README.md +42 -4
  2. package/dist/app-types.d.mts +1 -3
  3. package/dist/asset-manifest.mjs +13 -5
  4. package/dist/asset-manifest.mjs.map +1 -1
  5. package/dist/assets/{htmx.min.js → htmx.mjs} +10 -1
  6. package/dist/assets/htmx.mjs.map +1 -0
  7. package/dist/assets/mark.mjs +10 -0
  8. package/dist/assets/mark.mjs.map +1 -0
  9. package/dist/boot-tasks.mjs +80 -0
  10. package/dist/boot-tasks.mjs.map +1 -0
  11. package/dist/build-state-flags.mjs +18 -0
  12. package/dist/build-state-flags.mjs.map +1 -0
  13. package/dist/build-state-setup.mjs +75 -0
  14. package/dist/build-state-setup.mjs.map +1 -0
  15. package/dist/capture-alerts.mjs +46 -0
  16. package/dist/capture-alerts.mjs.map +1 -0
  17. package/dist/capture-process.mjs +59 -0
  18. package/dist/capture-process.mjs.map +1 -0
  19. package/dist/capture-setup.mjs +95 -117
  20. package/dist/capture-setup.mjs.map +1 -1
  21. package/dist/connections.mjs +34 -0
  22. package/dist/connections.mjs.map +1 -0
  23. package/dist/consumer-setup.mjs +58 -0
  24. package/dist/consumer-setup.mjs.map +1 -0
  25. package/dist/git-status.mjs +46 -0
  26. package/dist/git-status.mjs.map +1 -0
  27. package/dist/hidden-data.mjs +24 -0
  28. package/dist/hidden-data.mjs.map +1 -0
  29. package/dist/index.d.mts +3 -3
  30. package/dist/index.mjs +203 -125
  31. package/dist/index.mjs.map +1 -1
  32. package/dist/insights/budget.mjs +49 -0
  33. package/dist/insights/budget.mjs.map +1 -0
  34. package/dist/insights/deps.mjs +21 -0
  35. package/dist/insights/deps.mjs.map +1 -0
  36. package/dist/insights/errors.mjs +22 -0
  37. package/dist/insights/errors.mjs.map +1 -0
  38. package/dist/insights/health-panel.mjs +40 -0
  39. package/dist/insights/health-panel.mjs.map +1 -0
  40. package/dist/insights/job.mjs +103 -0
  41. package/dist/insights/job.mjs.map +1 -0
  42. package/dist/insights/load-health.mjs +59 -0
  43. package/dist/insights/load-health.mjs.map +1 -0
  44. package/dist/insights/load-triage.mjs +98 -0
  45. package/dist/insights/load-triage.mjs.map +1 -0
  46. package/dist/insights/panel.mjs +28 -0
  47. package/dist/insights/panel.mjs.map +1 -0
  48. package/dist/insights/request.mjs +113 -0
  49. package/dist/insights/request.mjs.map +1 -0
  50. package/dist/insights/stale.mjs +21 -0
  51. package/dist/insights/stale.mjs.map +1 -0
  52. package/dist/insights/usage-report.mjs +60 -0
  53. package/dist/insights/usage-report.mjs.map +1 -0
  54. package/dist/insights/view.mjs +48 -0
  55. package/dist/insights/view.mjs.map +1 -0
  56. package/dist/job-canceller.mjs +14 -0
  57. package/dist/job-canceller.mjs.map +1 -0
  58. package/dist/job-queue.mjs +51 -0
  59. package/dist/job-queue.mjs.map +1 -0
  60. package/dist/jobs-setup.mjs +132 -0
  61. package/dist/jobs-setup.mjs.map +1 -0
  62. package/dist/lifecycle.mjs +91 -19
  63. package/dist/lifecycle.mjs.map +1 -1
  64. package/dist/middleware/auth-gate.mjs +4 -2
  65. package/dist/middleware/auth-gate.mjs.map +1 -1
  66. package/dist/middleware/rate-limit.mjs +41 -33
  67. package/dist/middleware/rate-limit.mjs.map +1 -1
  68. package/dist/middleware/request-log.mjs +3 -2
  69. package/dist/middleware/request-log.mjs.map +1 -1
  70. package/dist/middleware/require-review.mjs +51 -0
  71. package/dist/middleware/require-review.mjs.map +1 -0
  72. package/dist/middleware/require-testing.mjs +41 -0
  73. package/dist/middleware/require-testing.mjs.map +1 -0
  74. package/dist/middleware/setup-gate.mjs.map +1 -1
  75. package/dist/middleware/store-scope.mjs +12 -2
  76. package/dist/middleware/store-scope.mjs.map +1 -1
  77. package/dist/notifications-pause.mjs +33 -0
  78. package/dist/notifications-pause.mjs.map +1 -0
  79. package/dist/notify.d.mts +2 -1
  80. package/dist/notify.mjs +78 -39
  81. package/dist/notify.mjs.map +1 -1
  82. package/dist/pages/a11y-violations.mjs +116 -0
  83. package/dist/pages/a11y-violations.mjs.map +1 -0
  84. package/dist/pages/admin-ai.mjs +78 -0
  85. package/dist/pages/admin-ai.mjs.map +1 -0
  86. package/dist/pages/admin-system-origin.mjs +123 -0
  87. package/dist/pages/admin-system-origin.mjs.map +1 -0
  88. package/dist/pages/admin-system-targets.mjs +53 -0
  89. package/dist/pages/admin-system-targets.mjs.map +1 -0
  90. package/dist/pages/admin-system.mjs +63 -13
  91. package/dist/pages/admin-system.mjs.map +1 -1
  92. package/dist/pages/build-detail.mjs +104 -76
  93. package/dist/pages/build-detail.mjs.map +1 -1
  94. package/dist/pages/build-diff-actions.mjs +87 -33
  95. package/dist/pages/build-diff-actions.mjs.map +1 -1
  96. package/dist/pages/build-diff-comments.mjs +16 -14
  97. package/dist/pages/build-diff-comments.mjs.map +1 -1
  98. package/dist/pages/build-diff-environment.mjs +82 -0
  99. package/dist/pages/build-diff-environment.mjs.map +1 -0
  100. package/dist/pages/build-diff-header.mjs +22 -4
  101. package/dist/pages/build-diff-header.mjs.map +1 -1
  102. package/dist/pages/build-diff-insight.mjs +113 -0
  103. package/dist/pages/build-diff-insight.mjs.map +1 -0
  104. package/dist/pages/build-diff-model.mjs +55 -10
  105. package/dist/pages/build-diff-model.mjs.map +1 -1
  106. package/dist/pages/build-diff-nav.mjs +28 -12
  107. package/dist/pages/build-diff-nav.mjs.map +1 -1
  108. package/dist/pages/build-diff-shortcuts.mjs +5 -1
  109. package/dist/pages/build-diff-shortcuts.mjs.map +1 -1
  110. package/dist/pages/build-diff-usage.mjs +63 -0
  111. package/dist/pages/build-diff-usage.mjs.map +1 -0
  112. package/dist/pages/build-diff-viewer.mjs +62 -5
  113. package/dist/pages/build-diff-viewer.mjs.map +1 -1
  114. package/dist/pages/build-diff.mjs +34 -13
  115. package/dist/pages/build-diff.mjs.map +1 -1
  116. package/dist/pages/build-tests.mjs +74 -0
  117. package/dist/pages/build-tests.mjs.map +1 -0
  118. package/dist/pages/component-detail.mjs +249 -0
  119. package/dist/pages/component-detail.mjs.map +1 -0
  120. package/dist/pages/component-stories.mjs +168 -0
  121. package/dist/pages/component-stories.mjs.map +1 -0
  122. package/dist/pages/compute-jobs.mjs +13 -26
  123. package/dist/pages/compute-jobs.mjs.map +1 -1
  124. package/dist/pages/design-links.mjs +182 -0
  125. package/dist/pages/design-links.mjs.map +1 -0
  126. package/dist/pages/environment-change.mjs +53 -0
  127. package/dist/pages/environment-change.mjs.map +1 -0
  128. package/dist/pages/health-panel.mjs +126 -0
  129. package/dist/pages/health-panel.mjs.map +1 -0
  130. package/dist/pages/library-grid.mjs +105 -0
  131. package/dist/pages/library-grid.mjs.map +1 -0
  132. package/dist/pages/library-model.mjs +47 -0
  133. package/dist/pages/library-model.mjs.map +1 -0
  134. package/dist/pages/library-snapshots.mjs +130 -0
  135. package/dist/pages/library-snapshots.mjs.map +1 -0
  136. package/dist/pages/library.mjs +112 -218
  137. package/dist/pages/library.mjs.map +1 -1
  138. package/dist/pages/notifications-paused.mjs +22 -0
  139. package/dist/pages/notifications-paused.mjs.map +1 -0
  140. package/dist/pages/profile.mjs +53 -48
  141. package/dist/pages/profile.mjs.map +1 -1
  142. package/dist/pages/project-builds-table.mjs +2 -2
  143. package/dist/pages/project-builds-table.mjs.map +1 -1
  144. package/dist/pages/project-builds.mjs +51 -21
  145. package/dist/pages/project-builds.mjs.map +1 -1
  146. package/dist/pages/project-settings.mjs +51 -12
  147. package/dist/pages/project-settings.mjs.map +1 -1
  148. package/dist/pages/projects-card.mjs +14 -1
  149. package/dist/pages/projects-card.mjs.map +1 -1
  150. package/dist/pages/projects.mjs +6 -2
  151. package/dist/pages/projects.mjs.map +1 -1
  152. package/dist/pages/review-changeset.mjs +116 -0
  153. package/dist/pages/review-changeset.mjs.map +1 -0
  154. package/dist/pages/review-comments.mjs +103 -0
  155. package/dist/pages/review-comments.mjs.map +1 -0
  156. package/dist/pages/review-verdict.mjs +102 -0
  157. package/dist/pages/review-verdict.mjs.map +1 -0
  158. package/dist/pages/settings-ai.mjs +69 -0
  159. package/dist/pages/settings-ai.mjs.map +1 -0
  160. package/dist/pages/settings-capture.mjs +124 -0
  161. package/dist/pages/settings-capture.mjs.map +1 -0
  162. package/dist/pages/settings-design-credential.mjs +99 -0
  163. package/dist/pages/settings-design-credential.mjs.map +1 -0
  164. package/dist/pages/settings-design-links.mjs +179 -0
  165. package/dist/pages/settings-design-links.mjs.map +1 -0
  166. package/dist/pages/settings-design.mjs +48 -0
  167. package/dist/pages/settings-design.mjs.map +1 -0
  168. package/dist/pages/settings-general.mjs +46 -21
  169. package/dist/pages/settings-general.mjs.map +1 -1
  170. package/dist/pages/settings-labels.mjs +43 -2
  171. package/dist/pages/settings-labels.mjs.map +1 -1
  172. package/dist/pages/settings-links.mjs +112 -0
  173. package/dist/pages/settings-links.mjs.map +1 -0
  174. package/dist/pages/settings-notifications-git.mjs +128 -0
  175. package/dist/pages/settings-notifications-git.mjs.map +1 -0
  176. package/dist/pages/settings-notifications.mjs +79 -42
  177. package/dist/pages/settings-notifications.mjs.map +1 -1
  178. package/dist/pages/settings-tests.mjs +33 -2
  179. package/dist/pages/settings-tests.mjs.map +1 -1
  180. package/dist/pages/target-choices.mjs +39 -0
  181. package/dist/pages/target-choices.mjs.map +1 -0
  182. package/dist/publish-setup.mjs +89 -0
  183. package/dist/publish-setup.mjs.map +1 -0
  184. package/dist/retention-timer.mjs +2 -1
  185. package/dist/retention-timer.mjs.map +1 -1
  186. package/dist/review-events.mjs +48 -0
  187. package/dist/review-events.mjs.map +1 -0
  188. package/dist/routers/admin-pages.mjs +45 -3
  189. package/dist/routers/admin-pages.mjs.map +1 -1
  190. package/dist/routers/admin-public-origin.mjs +85 -0
  191. package/dist/routers/admin-public-origin.mjs.map +1 -0
  192. package/dist/routers/assets.mjs +9 -2
  193. package/dist/routers/assets.mjs.map +1 -1
  194. package/dist/routers/auth.mjs +1 -1
  195. package/dist/routers/baseline-guard.mjs +2 -2
  196. package/dist/routers/baseline-guard.mjs.map +1 -1
  197. package/dist/routers/baselines.handlers.mjs +64 -0
  198. package/dist/routers/baselines.handlers.mjs.map +1 -0
  199. package/dist/routers/baselines.mjs +125 -0
  200. package/dist/routers/baselines.mjs.map +1 -0
  201. package/dist/routers/build-cancel.mjs +68 -0
  202. package/dist/routers/build-cancel.mjs.map +1 -0
  203. package/dist/routers/build-changeset.mjs +70 -0
  204. package/dist/routers/build-changeset.mjs.map +1 -0
  205. package/dist/routers/build-reviews.handlers.mjs +86 -0
  206. package/dist/routers/build-reviews.handlers.mjs.map +1 -0
  207. package/dist/routers/build-reviews.mjs +74 -0
  208. package/dist/routers/build-reviews.mjs.map +1 -0
  209. package/dist/routers/builds.handlers.mjs +46 -55
  210. package/dist/routers/builds.handlers.mjs.map +1 -1
  211. package/dist/routers/builds.mjs +71 -17
  212. package/dist/routers/builds.mjs.map +1 -1
  213. package/dist/routers/capture-config.mjs +35 -0
  214. package/dist/routers/capture-config.mjs.map +1 -0
  215. package/dist/routers/comments.mjs +41 -15
  216. package/dist/routers/comments.mjs.map +1 -1
  217. package/dist/routers/component-catalog.handlers.mjs +71 -0
  218. package/dist/routers/component-catalog.handlers.mjs.map +1 -0
  219. package/dist/routers/component.handlers.mjs +121 -0
  220. package/dist/routers/component.handlers.mjs.map +1 -0
  221. package/dist/routers/connections.mjs +118 -0
  222. package/dist/routers/connections.mjs.map +1 -0
  223. package/dist/routers/design-connect.handlers.mjs +44 -0
  224. package/dist/routers/design-connect.handlers.mjs.map +1 -0
  225. package/dist/routers/design.handlers.mjs +114 -0
  226. package/dist/routers/design.handlers.mjs.map +1 -0
  227. package/dist/routers/health-report.mjs +2 -3
  228. package/dist/routers/health-report.mjs.map +1 -1
  229. package/dist/routers/health.mjs +4 -3
  230. package/dist/routers/health.mjs.map +1 -1
  231. package/dist/routers/helpers.mjs +19 -3
  232. package/dist/routers/helpers.mjs.map +1 -1
  233. package/dist/routers/insights-ui.mjs +64 -0
  234. package/dist/routers/insights-ui.mjs.map +1 -0
  235. package/dist/routers/insights.handlers.mjs +45 -0
  236. package/dist/routers/insights.handlers.mjs.map +1 -0
  237. package/dist/routers/insights.mjs +189 -0
  238. package/dist/routers/insights.mjs.map +1 -0
  239. package/dist/routers/links.handlers.mjs +80 -0
  240. package/dist/routers/links.handlers.mjs.map +1 -0
  241. package/dist/routers/links.mjs +139 -0
  242. package/dist/routers/links.mjs.map +1 -0
  243. package/dist/routers/media.mjs +18 -4
  244. package/dist/routers/media.mjs.map +1 -1
  245. package/dist/routers/members.mjs +2 -1
  246. package/dist/routers/members.mjs.map +1 -1
  247. package/dist/routers/notification-channels.mjs +17 -23
  248. package/dist/routers/notification-channels.mjs.map +1 -1
  249. package/dist/routers/oauth-state.mjs +45 -0
  250. package/dist/routers/oauth-state.mjs.map +1 -0
  251. package/dist/routers/preview-base.mjs +38 -0
  252. package/dist/routers/preview-base.mjs.map +1 -0
  253. package/dist/routers/preview-token.mjs +51 -0
  254. package/dist/routers/preview-token.mjs.map +1 -0
  255. package/dist/routers/profile.mjs +3 -1
  256. package/dist/routers/profile.mjs.map +1 -1
  257. package/dist/routers/projects.mjs +17 -1
  258. package/dist/routers/projects.mjs.map +1 -1
  259. package/dist/routers/publish-upload.mjs +78 -0
  260. package/dist/routers/publish-upload.mjs.map +1 -0
  261. package/dist/routers/published-access.mjs +52 -0
  262. package/dist/routers/published-access.mjs.map +1 -0
  263. package/dist/routers/published-files.mjs +52 -0
  264. package/dist/routers/published-files.mjs.map +1 -0
  265. package/dist/routers/review-forms.mjs +72 -0
  266. package/dist/routers/review-forms.mjs.map +1 -0
  267. package/dist/routers/review-section.handlers.mjs +85 -0
  268. package/dist/routers/review-section.handlers.mjs.map +1 -0
  269. package/dist/routers/review-switch-off.mjs +29 -0
  270. package/dist/routers/review-switch-off.mjs.map +1 -0
  271. package/dist/routers/schemas.mjs +140 -22
  272. package/dist/routers/schemas.mjs.map +1 -1
  273. package/dist/routers/scoped-models.mjs +22 -0
  274. package/dist/routers/scoped-models.mjs.map +1 -0
  275. package/dist/routers/settings-ai.mjs +35 -0
  276. package/dist/routers/settings-ai.mjs.map +1 -0
  277. package/dist/routers/settings-capture.mjs +77 -0
  278. package/dist/routers/settings-capture.mjs.map +1 -0
  279. package/dist/routers/settings-design.mjs +103 -0
  280. package/dist/routers/settings-design.mjs.map +1 -0
  281. package/dist/routers/settings-general.mjs +55 -10
  282. package/dist/routers/settings-general.mjs.map +1 -1
  283. package/dist/routers/settings-git-channel.mjs +77 -0
  284. package/dist/routers/settings-git-channel.mjs.map +1 -0
  285. package/dist/routers/settings-labels.mjs +27 -1
  286. package/dist/routers/settings-labels.mjs.map +1 -1
  287. package/dist/routers/settings-links.mjs +63 -0
  288. package/dist/routers/settings-links.mjs.map +1 -0
  289. package/dist/routers/settings-members.mjs +4 -7
  290. package/dist/routers/settings-members.mjs.map +1 -1
  291. package/dist/routers/settings-notifications.mjs +15 -10
  292. package/dist/routers/settings-notifications.mjs.map +1 -1
  293. package/dist/routers/settings-tokens.mjs +2 -5
  294. package/dist/routers/settings-tokens.mjs.map +1 -1
  295. package/dist/routers/settings-webhooks.mjs +7 -24
  296. package/dist/routers/settings-webhooks.mjs.map +1 -1
  297. package/dist/routers/settings.handlers.mjs +107 -26
  298. package/dist/routers/settings.handlers.mjs.map +1 -1
  299. package/dist/routers/settings.mjs +16 -4
  300. package/dist/routers/settings.mjs.map +1 -1
  301. package/dist/routers/signed-storybook.mjs +53 -0
  302. package/dist/routers/signed-storybook.mjs.map +1 -0
  303. package/dist/routers/snapshot-groups.mjs +63 -0
  304. package/dist/routers/snapshot-groups.mjs.map +1 -0
  305. package/dist/routers/snapshots.mjs +103 -5
  306. package/dist/routers/snapshots.mjs.map +1 -1
  307. package/dist/routers/storybook.mjs +37 -88
  308. package/dist/routers/storybook.mjs.map +1 -1
  309. package/dist/routers/tokens.mjs +2 -5
  310. package/dist/routers/tokens.mjs.map +1 -1
  311. package/dist/routers/ui.mjs +34 -5
  312. package/dist/routers/ui.mjs.map +1 -1
  313. package/dist/routers/webhooks.mjs +5 -11
  314. package/dist/routers/webhooks.mjs.map +1 -1
  315. package/dist/routers/well-known.mjs +11 -4
  316. package/dist/routers/well-known.mjs.map +1 -1
  317. package/dist/routes/api-routes.mjs +93 -0
  318. package/dist/routes/api-routes.mjs.map +1 -0
  319. package/dist/routes/classification.mjs +38 -0
  320. package/dist/routes/classification.mjs.map +1 -0
  321. package/dist/routes/openapi-classes.mjs +22 -0
  322. package/dist/routes/openapi-classes.mjs.map +1 -0
  323. package/dist/routes/page-routes.mjs +104 -0
  324. package/dist/routes/page-routes.mjs.map +1 -0
  325. package/dist/runtime.mjs +18 -13
  326. package/dist/runtime.mjs.map +1 -1
  327. package/dist/serverless.mjs +5 -0
  328. package/dist/store.mjs +19 -1
  329. package/dist/store.mjs.map +1 -1
  330. package/dist/targets.mjs +46 -0
  331. package/dist/targets.mjs.map +1 -0
  332. package/dist/ui/buttons.mjs +16 -3
  333. package/dist/ui/buttons.mjs.map +1 -1
  334. package/dist/ui/design-reference.mjs +97 -0
  335. package/dist/ui/design-reference.mjs.map +1 -0
  336. package/dist/ui/document.mjs +1 -2
  337. package/dist/ui/document.mjs.map +1 -1
  338. package/dist/ui/forms.mjs +6 -5
  339. package/dist/ui/forms.mjs.map +1 -1
  340. package/dist/ui/icons/paths.mjs +1 -0
  341. package/dist/ui/icons/paths.mjs.map +1 -1
  342. package/dist/ui/preview-embed.mjs +82 -0
  343. package/dist/ui/preview-embed.mjs.map +1 -0
  344. package/dist/ui/shell/client-script.mjs +85 -0
  345. package/dist/ui/shell/client-script.mjs.map +1 -1
  346. package/dist/ui/shell/nav.mjs +12 -8
  347. package/dist/ui/shell/nav.mjs.map +1 -1
  348. package/dist/ui/shell/review-script.mjs +8 -0
  349. package/dist/ui/shell/review-script.mjs.map +1 -1
  350. package/dist/ui/shell/sidebar.mjs +2 -2
  351. package/dist/ui/shell/sidebar.mjs.map +1 -1
  352. package/package.json +11 -9
  353. package/dist/asset-paths.mjs +0 -23
  354. package/dist/asset-paths.mjs.map +0 -1
  355. package/dist/assets/mark.svg +0 -1
  356. package/dist/pages/settings-status.mjs +0 -137
  357. package/dist/pages/settings-status.mjs.map +0 -1
  358. package/dist/routers/settings-status.mjs +0 -77
  359. package/dist/routers/settings-status.mjs.map +0 -1
  360. package/dist/routers/status-configs.mjs +0 -108
  361. package/dist/routers/status-configs.mjs.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"schemas.mjs","names":[],"sources":["../../src/routers/schemas.ts"],"sourcesContent":["import { z } from \"@hono/zod-openapi\";\nimport { BROWSER_NAMES } from \"@storyshelf/core/adapter/capture-runner\";\nimport { BUILD_STATUSES, PROJECT_ROLES, SNAPSHOT_STATUSES } from \"@storyshelf/core/types\";\n\nconst storybookMetaSchema = z.record(z.string(), z.unknown()).nullable().optional();\n\n/** OpenAPI schema for a project record. */\nexport const projectSchema = z\n .object({\n id: z.string(),\n name: z.string(),\n slug: z.string(),\n gitRepository: z.string().nullable(),\n gitDefaultBranch: z.string(),\n pixelThreshold: z.number(),\n maxDiffRatio: z.number(),\n publicBranchRegex: z.string().nullable(),\n storybookMeta: z.string().nullable().optional(),\n executePlay: z.boolean(),\n playTimeoutMs: z.number(),\n // Stored rows predate enum validation, so the response stays a plain string.\n browser: z.string().optional(),\n createdAt: z.string(),\n updatedAt: z.string(),\n })\n .openapi(\"Project\");\n\n/** OpenAPI schema for creating a project. */\nexport const projectCreateSchema = z\n .object({\n name: z.string().min(1),\n gitRepository: z.string().optional(),\n gitDefaultBranch: z.string().optional(),\n storybookMeta: storybookMetaSchema,\n executePlay: z.boolean().optional(),\n playTimeoutMs: z.number().int().min(1000).max(30_000).optional(),\n })\n .openapi(\"ProjectCreateInput\");\n\n/** OpenAPI schema for patching a project. */\nexport const projectUpdateSchema = z\n .object({\n name: z.string().optional(),\n gitRepository: z.string().optional(),\n gitDefaultBranch: z.string().optional(),\n pixelThreshold: z.number().optional(),\n maxDiffRatio: z.number().optional(),\n publicBranchRegex: z.string().nullable().optional(),\n storybookMeta: storybookMetaSchema,\n executePlay: z.boolean().optional(),\n playTimeoutMs: z.number().int().min(1000).max(30_000).optional(),\n browser: z.enum(BROWSER_NAMES).optional(),\n })\n .openapi(\"ProjectUpdateInput\");\n\n/** OpenAPI schema for a build record. */\nexport const buildSchema = z\n .object({\n id: z.string(),\n projectId: z.string(),\n gitSha: z.string(),\n gitBranch: z.string(),\n isDefault: z.boolean(),\n authorEmail: z.string().nullable(),\n authorName: z.string().nullable(),\n message: z.string().nullable(),\n public: z.boolean(),\n status: z.enum(BUILD_STATUSES),\n snapshotCount: z.number(),\n changedCount: z.number(),\n approvedCount: z.number(),\n rejectedCount: z.number(),\n affectedOnly: z.boolean(),\n baselineSha: z.string().nullable(),\n changedFiles: z.string().nullable(),\n affectedImportPaths: z.string().nullable(),\n createdAt: z.string(),\n updatedAt: z.string(),\n })\n .openapi(\"Build\");\n\n/** OpenAPI schema for a snapshot record. */\nexport const snapshotSchema = z\n .object({\n id: z.string(),\n projectId: z.string(),\n buildId: z.string(),\n storyId: z.string(),\n storyName: z.string(),\n storyTitle: z.string(),\n storyImportPath: z.string().nullable(),\n viewportName: z.string(),\n viewportWidth: z.number(),\n viewportHeight: z.number(),\n screenshotPath: z.string(),\n diffPath: z.string().nullable(),\n diffPixels: z.number().nullable(),\n diffRatio: z.number().nullable(),\n diffPassed: z.boolean().nullable(),\n status: z.enum(SNAPSHOT_STATUSES),\n reviewedBy: z.string().nullable(),\n reviewedAt: z.string().nullable(),\n inherited: z.boolean(),\n createdAt: z.string(),\n updatedAt: z.string(),\n })\n .openapi(\"Snapshot\");\n\n/** OpenAPI schema for a capture attempt (one run of a build's capture). */\nexport const attemptSchema = z\n .object({\n id: z.string(),\n projectId: z.string(),\n buildId: z.string(),\n attemptNo: z.number(),\n status: z.enum([\"queued\", \"running\", \"completed\", \"failed\", \"cancelled\"]),\n error: z.string().nullable(),\n reqId: z.string().nullable(),\n storyCount: z.number(),\n failedCount: z.number(),\n queuedAt: z.string(),\n startedAt: z.string().nullable(),\n finishedAt: z.string().nullable(),\n createdAt: z.string(),\n updatedAt: z.string(),\n })\n .openapi(\"CaptureAttempt\");\n\n/** OpenAPI schema for a single capture log line within an attempt. */\nexport const attemptLogSchema = z\n .object({\n id: z.string(),\n projectId: z.string(),\n buildId: z.string(),\n attemptId: z.string(),\n seq: z.number(),\n level: z.string(),\n message: z.string(),\n fields: z.string().nullable(),\n createdAt: z.string(),\n })\n .openapi(\"CaptureLog\");\n\n/** OpenAPI schema for a build comment. */\nexport const commentSchema = z\n .object({\n id: z.string(),\n projectId: z.string(),\n buildId: z.string(),\n snapshotId: z.string().nullable(),\n userId: z.string().nullable(),\n body: z.string(),\n parentId: z.string().nullable(),\n resolved: z.boolean(),\n createdAt: z.string(),\n updatedAt: z.string(),\n })\n .openapi(\"Comment\");\n\n/** OpenAPI schema for creating a build comment. */\nexport const commentCreateSchema = z\n .object({\n body: z.string().min(1),\n snapshotId: z.string().optional(),\n parentId: z.string().optional(),\n })\n .openapi(\"CommentCreateInput\");\n\n/** OpenAPI schema for a project label type. */\nexport const labelTypeSchema = z\n .object({\n id: z.string(),\n projectId: z.string(),\n key: z.string(),\n name: z.string(),\n linkTemplate: z.string().nullable(),\n color: z.string().nullable(),\n createdAt: z.string(),\n })\n .openapi(\"LabelType\");\n\n/** OpenAPI schema for creating a label type. */\nexport const labelTypeCreateSchema = z\n .object({\n key: z.string().min(1),\n name: z.string().min(1),\n linkTemplate: z.string().optional(),\n color: z.string().optional(),\n })\n .openapi(\"LabelTypeCreateInput\");\n\n/** OpenAPI schema for patching a label type. */\nexport const labelTypeUpdateSchema = z\n .object({\n name: z.string().min(1).optional(),\n linkTemplate: z.string().nullable().optional(),\n color: z.string().nullable().optional(),\n })\n .openapi(\"LabelTypeUpdateInput\");\n\n/** OpenAPI schema for a public API token (hash omitted). */\nexport const tokenPublicSchema = z\n .object({\n id: z.string(),\n projectId: z.string(),\n name: z.string(),\n lastUsedAt: z.string().nullable(),\n createdAt: z.string(),\n })\n .openapi(\"Token\");\n\n/** OpenAPI schema for creating an API token. */\nexport const tokenCreateSchema = z.object({ name: z.string().min(1) }).openapi(\"TokenCreateInput\");\n\n/** OpenAPI schema for a newly created token including its secret value. */\nexport const tokenCreatedSchema = z\n .object({\n name: z.string(),\n token: z.string(),\n })\n .openapi(\"TokenCreated\");\n\n/** OpenAPI schema for a project member record. */\nexport const memberSchema = z\n .object({\n id: z.string(),\n projectId: z.string(),\n userId: z.string(),\n role: z.enum(PROJECT_ROLES),\n createdAt: z.string(),\n })\n .openapi(\"ProjectMember\");\n\n/** OpenAPI schema for updating a member's role. */\nexport const memberRoleSchema = z\n .object({ role: z.enum(PROJECT_ROLES) })\n .openapi(\"MemberRoleInput\");\n\n/** OpenAPI schema for adding a member to a project. */\nexport const memberSetSchema = z\n .object({\n userId: z.string().min(1),\n role: z.enum(PROJECT_ROLES),\n })\n .openapi(\"MemberSetInput\");\n\n/** OpenAPI schema for an identity-provider group mapping. */\nexport const groupMappingSchema = z\n .object({\n id: z.string(),\n projectId: z.string(),\n groupName: z.string(),\n role: z.enum(PROJECT_ROLES),\n createdAt: z.string(),\n })\n .openapi(\"GroupMapping\");\n\n/** OpenAPI schema for creating a group mapping. */\nexport const groupMappingCreateSchema = z\n .object({\n groupName: z.string().min(1),\n role: z.enum(PROJECT_ROLES),\n })\n .openapi(\"GroupMappingCreateInput\");\n\n/** OpenAPI schema for a public webhook (secret omitted). */\nexport const webhookPublicSchema = z\n .object({\n id: z.string(),\n url: z.string(),\n events: z.array(z.string()),\n })\n .openapi(\"Webhook\");\n\n/** OpenAPI schema for creating a webhook. */\nexport const webhookCreateSchema = z\n .object({\n // oxlint-disable-next-line typescript/no-deprecated -- z.string().url() kept for zod v3 API compat\n url: z.string().url(),\n events: z.array(z.string().min(1)).optional(),\n })\n .openapi(\"WebhookCreateInput\");\n\n/** OpenAPI schema for a newly created webhook including its signing secret. */\nexport const webhookCreatedSchema = z\n .object({\n id: z.string(),\n url: z.string(),\n events: z.array(z.string()),\n secret: z.string(),\n })\n .openapi(\"WebhookCreated\");\n\n/** OpenAPI schema for a git status-check configuration. */\nexport const statusConfigSchema = z\n .object({\n id: z.string(),\n projectId: z.string(),\n provider: z.string(),\n config: z.record(z.string(), z.unknown()),\n hasToken: z.boolean(),\n createdAt: z.string(),\n updatedAt: z.string(),\n })\n .openapi(\"StatusConfig\");\n\n/** OpenAPI schema for creating a git status-check configuration. */\nexport const statusConfigCreateSchema = z\n .object({\n provider: z.string().min(1),\n config: z.record(z.string(), z.unknown()),\n token: z.string().min(1),\n })\n .openapi(\"StatusConfigCreateInput\");\n\n/** OpenAPI schema for a public notification channel (secret omitted). */\nexport const notificationChannelSchema = z\n .object({\n id: z.string(),\n projectId: z.string().nullable(),\n provider: z.string(),\n config: z.record(z.string(), z.unknown()),\n hasSecret: z.boolean(),\n events: z.array(z.string()),\n enabled: z.boolean(),\n createdAt: z.string(),\n updatedAt: z.string(),\n })\n .openapi(\"NotificationChannel\");\n\n/** OpenAPI schema for creating a notification channel. */\nexport const notificationChannelCreateSchema = z\n .object({\n provider: z.string().min(1),\n config: z.record(z.string(), z.unknown()),\n secret: z.string().min(1).optional(),\n events: z.array(z.string().min(1)).optional(),\n enabled: z.boolean().optional(),\n })\n .openapi(\"NotificationChannelCreateInput\");\n\n/** OpenAPI schema for a per-user notification subscription. */\nexport const notificationSubscriptionSchema = z\n .object({\n projectId: z.string(),\n userId: z.string(),\n events: z.array(z.string()),\n via: z.array(z.string()),\n enabled: z.boolean(),\n })\n .openapi(\"NotificationSubscription\");\n\n/** OpenAPI schema for setting your own notification subscription. */\nexport const notificationSubscriptionPutSchema = z\n .object({\n events: z.array(z.string().min(1)).optional(),\n via: z.array(z.string().min(1)).optional(),\n enabled: z.boolean().optional(),\n })\n .openapi(\"NotificationSubscriptionPutInput\");\n\n/** OpenAPI schema for a generic `{ ok: true }` acknowledgement. */\nexport const okSchema = z.object({ ok: z.boolean() }).openapi(\"Ok\");\n\n/** Bulk approve result: ids skipped because their baseline changed after the diff. */\nexport const approveAllResultSchema = z\n .object({ ok: z.boolean(), skipped: z.array(z.string()) })\n .openapi(\"ApproveAllResult\");\n\n/** Result of re-diffing a build against the baselines that apply now. */\nexport const rediffResultSchema = z\n .object({\n ok: z.boolean(),\n rediffed: z.number().int(),\n statusChanged: z.number().int(),\n missingScreenshots: z.array(z.string()),\n })\n .openapi(\"RediffResult\");\n\n/** 409 returned when approving a snapshot whose baseline changed after the diff. */\nexport const baselineChangedSchema = z\n .object({\n code: z.literal(\"baseline_changed\"),\n message: z.string(),\n snapshotId: z.string(),\n baselineStatus: z.enum([\"stale\", \"removed\"]),\n })\n .openapi(\"BaselineChanged\");\n\n/** OpenAPI schema for an error response message. */\nexport const errorSchema = z.object({ message: z.string() }).openapi(\"Error\");\n\n/** OpenAPI schema for the retention purge result. */\nexport const purgeSchema = z\n .object({\n removedBuilds: z.number(),\n removedBranches: z.number().optional(),\n removedBaselines: z.number().optional(),\n })\n .openapi(\"PurgeResult\");\n\n/** OpenAPI schema for the retention purge input. */\nexport const purgeInputSchema = z.object({ ttlDays: z.number().optional() }).openapi(\"PurgeInput\");\n\n/** Shared response spec for a 400 Bad Request. */\nexport const badRequest = {\n 400: { content: { \"application/json\": { schema: errorSchema } }, description: \"Bad request\" },\n} as const;\n\n/** Shared response spec for a 401 Unauthorized. */\nexport const unauthorized = {\n 401: { content: { \"application/json\": { schema: errorSchema } }, description: \"Unauthorized\" },\n} as const;\n\n/** Shared response spec for a 403 Forbidden. */\nexport const forbidden = {\n 403: { content: { \"application/json\": { schema: errorSchema } }, description: \"Forbidden\" },\n} as const;\n\n/** Shared response spec for a 404 Not Found. */\nexport const notFound = {\n 404: { content: { \"application/json\": { schema: errorSchema } }, description: \"Not found\" },\n} as const;\n"],"mappings":"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"}
1
+ {"version":3,"file":"schemas.mjs","names":[],"sources":["../../src/routers/schemas.ts"],"sourcesContent":["import { z } from \"@hono/zod-openapi\";\nimport { BROWSER_NAMES, TARGET_NAME_PATTERN } from \"@storyshelf/core/adapter/capture-runner\";\nimport { WebhookModel } from \"@storyshelf/core/models\";\nimport {\n BUILD_STATUSES,\n PROJECT_ROLES,\n REVIEW_VERDICTS,\n SNAPSHOT_STATUSES,\n} from \"@storyshelf/core/types\";\nimport { storybookUrlTtlOverrideError } from \"@storyshelf/core/utils\";\n\n/** A capture target: an engine (`chromium`) or a remote one, `<engine>@<name>` (ADR 0007). */\nconst captureTargetSchema = z\n .string()\n .regex(\n new RegExp(\n `^(?:${BROWSER_NAMES.join(\"|\")})(?:@${TARGET_NAME_PATTERN.source.slice(1, -1)})?$`,\n \"u\",\n ),\n \"Expected an engine (chromium, firefox, webkit, chrome) or <engine>@<name>\",\n );\n\nconst reviewApprovalsSchema = z.number().int().min(1).max(20);\n\n/** Seconds a signed Storybook URL stays valid (ADR 0007, decision 5). */\nconst storybookUrlTtlSchema = z\n .number()\n .int()\n .superRefine((seconds, ctx) => {\n const message = storybookUrlTtlOverrideError(seconds);\n if (message !== undefined) {\n ctx.addIssue({ code: \"custom\", message });\n }\n });\n\nconst storybookMetaSchema = z.record(z.string(), z.unknown()).nullable().optional();\n\n/** OpenAPI schema for a project record. */\nexport const projectSchema = z\n .object({\n id: z.string(),\n name: z.string(),\n slug: z.string(),\n gitRepository: z.string().nullable(),\n gitDefaultBranch: z.string(),\n pixelThreshold: z.number(),\n maxDiffRatio: z.number(),\n publicBranchRegex: z.string().nullable(),\n storybookMeta: z.string().nullable().optional(),\n executePlay: z.boolean(),\n playTimeoutMs: z.number(),\n /** Axe accessibility checks run during capture (default on; results never block a build). */\n runA11y: z.boolean().optional(),\n /** Storybook 10 `.test()` entries run as interaction tests during capture (default off). */\n runTests: z.boolean().optional(),\n // Stored rows predate enum validation, so the response stays a plain string.\n browser: z.string().optional(),\n /** JSON array of capture targets, primary first, primary first; null = just `browser`. */\n browsers: z.string().nullable().optional(),\n /** JSON array of mode names to capture; null = every declared mode. */\n modesAllowlist: z.string().nullable().optional(),\n /** AI profile gate: null/absent = AI off for this project. */\n aiProfile: z.string().nullable().optional(),\n /** Review is on for this project (ADR 0013, decision 1). */\n reviewEnabled: z.boolean().optional(),\n /** Approvals a build needs when `testing` is off. */\n reviewApprovals: z.number().optional(),\n /** Seconds the signed Storybook URLs of a render stay valid; null derives it from the stories. */\n storybookUrlTtlSeconds: z.number().nullable().optional(),\n createdAt: z.string(),\n updatedAt: z.string(),\n })\n .openapi(\"Project\");\n\n/** OpenAPI schema for creating a project. */\nexport const projectCreateSchema = z\n .object({\n name: z.string().min(1),\n gitRepository: z.string().optional(),\n gitDefaultBranch: z.string().optional(),\n storybookMeta: storybookMetaSchema,\n executePlay: z.boolean().optional(),\n playTimeoutMs: z.number().int().min(1000).max(30_000).optional(),\n /** Axe accessibility checks during capture; on when omitted. */\n runA11y: z.boolean().optional(),\n /** Run Storybook 10 `.test()` entries as interaction tests during capture; off when omitted. */\n runTests: z.boolean().optional(),\n reviewEnabled: z.boolean().optional(),\n reviewApprovals: reviewApprovalsSchema.optional(),\n })\n .openapi(\"ProjectCreateInput\");\n\n/** OpenAPI schema for patching a project. */\nexport const projectUpdateSchema = z\n .object({\n name: z.string().optional(),\n gitRepository: z.string().optional(),\n gitDefaultBranch: z.string().optional(),\n pixelThreshold: z.number().optional(),\n maxDiffRatio: z.number().optional(),\n publicBranchRegex: z.string().nullable().optional(),\n storybookMeta: storybookMetaSchema,\n executePlay: z.boolean().optional(),\n playTimeoutMs: z.number().int().min(1000).max(30_000).optional(),\n /** Turn axe accessibility checks during capture on or off (admins). */\n runA11y: z.boolean().optional(),\n /** Turn the Storybook 10 `.test()` runs on or off (admins). */\n runTests: z.boolean().optional(),\n /** Single capture target; clears `browsers`. */\n browser: captureTargetSchema.optional(),\n /** Capture targets, primary first (max 3); null returns to single `browser`. */\n browsers: z.array(captureTargetSchema).min(1).max(3).nullable().optional(),\n /** Mode names to capture; null runs every declared mode. */\n modesAllowlist: z.array(z.string().min(1)).max(50).nullable().optional(),\n /** Site admins only: a configured AI profile name, or null to turn AI off. */\n aiProfile: z.string().min(1).nullable().optional(),\n /** Turn review (comments and build-level verdicts) on or off for the project. */\n reviewEnabled: z.boolean().optional(),\n /** Approvals a build needs before it is accepted when `testing` is off. */\n reviewApprovals: reviewApprovalsSchema.optional(),\n /** Seconds (at least one minimum slice plus grace, 1050, to 86400) the signed Storybook URLs of a render stay valid; null derives it from the stories. */\n storybookUrlTtlSeconds: storybookUrlTtlSchema.nullable().optional(),\n })\n .openapi(\"ProjectUpdateInput\");\n\n/** OpenAPI schema for a build record. */\nexport const buildSchema = z\n .object({\n id: z.string(),\n projectId: z.string(),\n gitSha: z.string(),\n gitBranch: z.string(),\n isDefault: z.boolean(),\n authorEmail: z.string().nullable(),\n authorName: z.string().nullable(),\n message: z.string().nullable(),\n public: z.boolean(),\n status: z.enum(BUILD_STATUSES),\n snapshotCount: z.number(),\n changedCount: z.number(),\n approvedCount: z.number(),\n rejectedCount: z.number(),\n inheritedCount: z.number(),\n affectedOnly: z.boolean(),\n baselineSha: z.string().nullable(),\n changedFiles: z.string().nullable(),\n affectedImportPaths: z.string().nullable(),\n /** JSON array of the build's Storybook `.test()` results (id, title, name, status, error); null when none ran. */\n testResults: z.string().nullable().optional(),\n createdAt: z.string(),\n updatedAt: z.string(),\n })\n .openapi(\"Build\");\n\n/** OpenAPI schema for a snapshot record. */\nexport const snapshotSchema = z\n .object({\n id: z.string(),\n projectId: z.string(),\n buildId: z.string(),\n storyId: z.string(),\n storyName: z.string(),\n storyTitle: z.string(),\n storyImportPath: z.string().nullable(),\n viewportName: z.string(),\n /** Globals JSON the capture's mode applied; null for plain viewports. */\n mode: z.string().nullable().optional(),\n viewportWidth: z.number(),\n viewportHeight: z.number(),\n screenshotPath: z.string(),\n diffPath: z.string().nullable(),\n diffPixels: z.number().nullable(),\n diffRatio: z.number().nullable(),\n diffPassed: z.boolean().nullable(),\n status: z.enum(SNAPSHOT_STATUSES),\n reviewedBy: z.string().nullable(),\n reviewedAt: z.string().nullable(),\n inherited: z.boolean(),\n /** JSON array of the axe violations found on this capture (non-blocking); null when none. */\n a11yViolations: z.string().nullable().optional(),\n createdAt: z.string(),\n updatedAt: z.string(),\n })\n .openapi(\"Snapshot\");\n\n/** OpenAPI schema for a capture attempt (one run of a build's capture). */\nexport const attemptSchema = z\n .object({\n id: z.string(),\n projectId: z.string(),\n buildId: z.string(),\n attemptNo: z.number(),\n status: z.enum([\"queued\", \"running\", \"completed\", \"failed\", \"cancelled\"]),\n error: z.string().nullable(),\n reqId: z.string().nullable(),\n storyCount: z.number(),\n failedCount: z.number(),\n queuedAt: z.string(),\n startedAt: z.string().nullable(),\n finishedAt: z.string().nullable(),\n createdAt: z.string(),\n updatedAt: z.string(),\n })\n .openapi(\"CaptureAttempt\");\n\n/** OpenAPI schema for a single capture log line within an attempt. */\nexport const attemptLogSchema = z\n .object({\n id: z.string(),\n projectId: z.string(),\n buildId: z.string(),\n attemptId: z.string(),\n seq: z.number(),\n level: z.string(),\n message: z.string(),\n fields: z.string().nullable(),\n createdAt: z.string(),\n })\n .openapi(\"CaptureLog\");\n\n/** OpenAPI schema for a build comment. */\nexport const commentSchema = z\n .object({\n id: z.string(),\n projectId: z.string(),\n buildId: z.string(),\n snapshotId: z.string().nullable(),\n /** Storybook story id of a story-anchored comment. */\n storyId: z.string().nullable().optional(),\n userId: z.string().nullable(),\n body: z.string(),\n parentId: z.string().nullable(),\n resolved: z.boolean(),\n createdAt: z.string(),\n updatedAt: z.string(),\n })\n .openapi(\"Comment\");\n\n/** OpenAPI schema for creating a build comment. */\nexport const commentCreateSchema = z\n .object({\n body: z.string().min(1),\n /** Anchor to a snapshot; not together with `storyId`. */\n snapshotId: z.string().optional(),\n /** Anchor to a story of the build; not together with `snapshotId`. */\n storyId: z.string().min(1).optional(),\n parentId: z.string().optional(),\n })\n .openapi(\"CommentCreateInput\");\n\n/** OpenAPI schema for a build-level review verdict. */\nexport const buildReviewSchema = z\n .object({\n id: z.string(),\n buildId: z.string(),\n userId: z.string().nullable(),\n verdict: z.enum(REVIEW_VERDICTS),\n body: z.string().nullable(),\n createdAt: z.string(),\n })\n .openapi(\"BuildReview\");\n\n/** OpenAPI schema for giving a build a verdict. */\nexport const buildReviewCreateSchema = z\n .object({\n verdict: z.enum(REVIEW_VERDICTS),\n body: z.string().max(4000).optional(),\n })\n .openapi(\"BuildReviewInput\");\n\n/** OpenAPI schema for a build's review state: the verdicts and what they make of the build. */\nexport const buildReviewStateSchema = z\n .object({\n /** Build status now. */\n status: z.enum(BUILD_STATUSES),\n /** Approvals the project requires. */\n approvalsRequired: z.number(),\n /** Approvals among the reviewers' latest verdicts. */\n approvals: z.number(),\n /** Every reviewer's latest verdict. */\n latest: buildReviewSchema.array(),\n /** Every verdict given, oldest first. */\n history: buildReviewSchema.array(),\n })\n .openapi(\"BuildReviewState\");\n\n/** OpenAPI schema for a project label type. */\nexport const labelTypeSchema = z\n .object({\n id: z.string(),\n projectId: z.string(),\n key: z.string(),\n name: z.string(),\n linkTemplate: z.string().nullable(),\n color: z.string().nullable(),\n createdAt: z.string(),\n })\n .openapi(\"LabelType\");\n\n/** OpenAPI schema for creating a label type. */\nexport const labelTypeCreateSchema = z\n .object({\n key: z.string().min(1),\n name: z.string().min(1),\n linkTemplate: z.string().optional(),\n color: z.string().optional(),\n })\n .openapi(\"LabelTypeCreateInput\");\n\n/** OpenAPI schema for patching a label type. */\nexport const labelTypeUpdateSchema = z\n .object({\n name: z.string().min(1).optional(),\n linkTemplate: z.string().nullable().optional(),\n color: z.string().nullable().optional(),\n })\n .openapi(\"LabelTypeUpdateInput\");\n\n/** OpenAPI schema for a public API token (hash omitted). */\nexport const tokenPublicSchema = z\n .object({\n id: z.string(),\n projectId: z.string(),\n name: z.string(),\n lastUsedAt: z.string().nullable(),\n createdAt: z.string(),\n })\n .openapi(\"Token\");\n\n/** OpenAPI schema for creating an API token. */\nexport const tokenCreateSchema = z.object({ name: z.string().min(1) }).openapi(\"TokenCreateInput\");\n\n/** OpenAPI schema for a newly created token including its secret value. */\nexport const tokenCreatedSchema = z\n .object({\n name: z.string(),\n token: z.string(),\n })\n .openapi(\"TokenCreated\");\n\n/** OpenAPI schema for a project member record. */\nexport const memberSchema = z\n .object({\n id: z.string(),\n projectId: z.string(),\n userId: z.string(),\n role: z.enum(PROJECT_ROLES),\n createdAt: z.string(),\n })\n .openapi(\"ProjectMember\");\n\n/** OpenAPI schema for updating a member's role. */\nexport const memberRoleSchema = z\n .object({ role: z.enum(PROJECT_ROLES) })\n .openapi(\"MemberRoleInput\");\n\n/** OpenAPI schema for adding a member to a project. */\nexport const memberSetSchema = z\n .object({\n userId: z.string().min(1),\n role: z.enum(PROJECT_ROLES),\n })\n .openapi(\"MemberSetInput\");\n\n/** OpenAPI schema for an identity-provider group mapping. */\nexport const groupMappingSchema = z\n .object({\n id: z.string(),\n projectId: z.string(),\n groupName: z.string(),\n role: z.enum(PROJECT_ROLES),\n createdAt: z.string(),\n })\n .openapi(\"GroupMapping\");\n\n/** OpenAPI schema for creating a group mapping. */\nexport const groupMappingCreateSchema = z\n .object({\n groupName: z.string().min(1),\n role: z.enum(PROJECT_ROLES),\n })\n .openapi(\"GroupMappingCreateInput\");\n\n/** OpenAPI schema for a public webhook (secret omitted). */\nexport const webhookPublicSchema = z\n .object({\n id: z.string(),\n url: z.string(),\n events: z.array(z.string()),\n })\n .openapi(\"Webhook\");\n\n// oxlint-disable-next-line typescript/no-deprecated -- z.string().url() kept for zod v3 API compat\nconst webhookUrlBase = z.string().url();\n\n/** OpenAPI schema for creating a webhook. */\nexport const webhookCreateSchema = z\n .object({\n url: webhookUrlBase.refine((url) => WebhookModel.urlError(url) === null, {\n message: \"URL must use http or https\",\n }),\n events: z.array(z.string().min(1)).optional(),\n })\n .openapi(\"WebhookCreateInput\");\n\n/** OpenAPI schema for a newly created webhook including its signing secret. */\nexport const webhookCreatedSchema = z\n .object({\n id: z.string(),\n url: z.string(),\n events: z.array(z.string()),\n secret: z.string(),\n })\n .openapi(\"WebhookCreated\");\n\n/** OpenAPI schema for a public notification channel (secret omitted). */\nexport const notificationChannelSchema = z\n .object({\n id: z.string(),\n projectId: z.string().nullable(),\n provider: z.string(),\n config: z.record(z.string(), z.unknown()),\n hasSecret: z.boolean(),\n events: z.array(z.string()),\n enabled: z.boolean(),\n lastError: z.string().nullable(),\n lastErrorAt: z.string().nullable(),\n createdAt: z.string(),\n updatedAt: z.string(),\n })\n .openapi(\"NotificationChannel\");\n\n/** OpenAPI schema for creating a notification channel. */\nexport const notificationChannelCreateSchema = z\n .object({\n provider: z.string().min(1),\n config: z.record(z.string(), z.unknown()),\n secret: z.string().min(1).optional(),\n events: z.array(z.string().min(1)).optional(),\n enabled: z.boolean().optional(),\n })\n .openapi(\"NotificationChannelCreateInput\");\n\n/** OpenAPI schema for a per-user notification subscription. */\nexport const notificationSubscriptionSchema = z\n .object({\n projectId: z.string(),\n userId: z.string(),\n events: z.array(z.string()),\n via: z.array(z.string()),\n enabled: z.boolean(),\n })\n .openapi(\"NotificationSubscription\");\n\n/** OpenAPI schema for setting your own notification subscription. */\nexport const notificationSubscriptionPutSchema = z\n .object({\n events: z.array(z.string().min(1)).optional(),\n via: z.array(z.string().min(1)).optional(),\n enabled: z.boolean().optional(),\n })\n .openapi(\"NotificationSubscriptionPutInput\");\n\n/** OpenAPI schema for a generic `{ ok: true }` acknowledgement. */\nexport const okSchema = z.object({ ok: z.boolean() }).openapi(\"Ok\");\n\n/** Bulk approve result: ids skipped because their baseline changed after the diff. */\nexport const approveAllResultSchema = z\n .object({ ok: z.boolean(), skipped: z.array(z.string()) })\n .openapi(\"ApproveAllResult\");\n\n/** Group decision result: the snapshots one story/mode decision covered. */\nexport const groupDecisionResultSchema = z\n .object({ ok: z.boolean(), snapshotIds: z.array(z.string()) })\n .openapi(\"GroupDecisionResult\");\n\n/** Result of re-diffing a build against the baselines that apply now. */\nexport const rediffResultSchema = z\n .object({\n ok: z.boolean(),\n rediffed: z.number().int(),\n statusChanged: z.number().int(),\n missingScreenshots: z.array(z.string()),\n })\n .openapi(\"RediffResult\");\n\n/** 409 returned when approving a snapshot whose baseline changed after the diff. */\nexport const baselineChangedSchema = z\n .object({\n code: z.literal(\"baseline_changed\"),\n message: z.string(),\n snapshotId: z.string(),\n baselineStatus: z.enum([\"stale\", \"removed\"]),\n })\n .openapi(\"BaselineChanged\");\n\n/** OpenAPI schema for an error response message. */\nexport const errorSchema = z.object({ message: z.string() }).openapi(\"Error\");\n\n/** OpenAPI schema for the retention purge result. */\nexport const purgeSchema = z\n .object({\n removedBuilds: z.number(),\n removedBranches: z.number().optional(),\n removedBaselines: z.number().optional(),\n })\n .openapi(\"PurgeResult\");\n\n/** OpenAPI schema for the retention purge input. */\nexport const purgeInputSchema = z.object({ ttlDays: z.number().optional() }).openapi(\"PurgeInput\");\n\n/** Shared response spec for a 400 Bad Request. */\nexport const badRequest = {\n 400: { content: { \"application/json\": { schema: errorSchema } }, description: \"Bad request\" },\n} as const;\n\n/** Shared response spec for a 401 Unauthorized. */\nexport const unauthorized = {\n 401: { content: { \"application/json\": { schema: errorSchema } }, description: \"Unauthorized\" },\n} as const;\n\n/** Shared response spec for a 403 Forbidden. */\nexport const forbidden = {\n 403: { content: { \"application/json\": { schema: errorSchema } }, description: \"Forbidden\" },\n} as const;\n\n/** Shared response spec for a 404 Not Found. */\nexport const notFound = {\n 404: { content: { \"application/json\": { schema: errorSchema } }, description: \"Not found\" },\n} as const;\n\n/** OpenAPI schema for an insight (build triage or project health). */\nexport const insightSchema = z\n .object({\n id: z.string(),\n kind: z.enum([\"triage\", \"health\"]),\n buildId: z.string().nullable(),\n window: z.string().nullable(),\n status: z.enum([\"pending\", \"running\", \"done\", \"failed\"]),\n profile: z.string(),\n model: z.string(),\n promptVersion: z.string(),\n verdict: z.string().nullable(),\n summary: z.string().nullable(),\n output: z.unknown().nullable(),\n meta: z.unknown().nullable(),\n errorCode: z.string().nullable(),\n createdAt: z.string(),\n startedAt: z.string().nullable(),\n finishedAt: z.string().nullable(),\n })\n .openapi(\"Insight\");\n\n/** Request body to (re)generate a build triage. */\nexport const insightRequestSchema = z\n .object({\n task: z.literal(\"triage\").optional(),\n force: z.boolean().optional(),\n profile: z.string().min(1).optional(),\n })\n .openapi(\"InsightRequest\");\n\n/** Request body to (re)generate a project-health digest. */\nexport const healthRequestSchema = z\n .object({ window: z.string().optional(), force: z.boolean().optional() })\n .openapi(\"HealthInsightRequest\");\n"],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { credentialKeys } from "../credential-keys.mjs";
5
+ import { getStore } from "../store.mjs";
6
+ import { NotificationChannelModel, WebhookModel } from "@storyshelf/core/models";
7
+ //#region src/routers/scoped-models.ts
8
+ /** Models that encrypt secrets at rest, bound to the current request's db and server secret. */
9
+ /** Webhook model for the current request. */
10
+ function webhookModel() {
11
+ const { db, config } = getStore();
12
+ return new WebhookModel(db, void 0, credentialKeys(config));
13
+ }
14
+ /** Notification-channel model for the current request. */
15
+ function notificationChannelModel() {
16
+ const { db, config } = getStore();
17
+ return new NotificationChannelModel(db, void 0, credentialKeys(config));
18
+ }
19
+ //#endregion
20
+ export { notificationChannelModel, webhookModel };
21
+
22
+ //# sourceMappingURL=scoped-models.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scoped-models.mjs","names":[],"sources":["../../src/routers/scoped-models.ts"],"sourcesContent":["import { NotificationChannelModel, WebhookModel } from \"@storyshelf/core/models\";\nimport { credentialKeys } from \"../credential-keys.ts\";\nimport { getStore } from \"../store.ts\";\n\n/** Models that encrypt secrets at rest, bound to the current request's db and server secret. */\n\n/** Webhook model for the current request. */\nexport function webhookModel(): WebhookModel {\n const { db, config } = getStore();\n return new WebhookModel(db, undefined, credentialKeys(config));\n}\n\n/** Notification-channel model for the current request. */\nexport function notificationChannelModel(): NotificationChannelModel {\n const { db, config } = getStore();\n return new NotificationChannelModel(db, undefined, credentialKeys(config));\n}\n"],"mappings":";;;;;;;;;AAOA,SAAgB,eAA6B;CAC3C,MAAM,EAAE,IAAI,WAAW,SAAS;CAChC,OAAO,IAAI,aAAa,IAAI,KAAA,GAAW,eAAe,MAAM,CAAC;AAC/D;;AAGA,SAAgB,2BAAqD;CACnE,MAAM,EAAE,IAAI,WAAW,SAAS;CAChC,OAAO,IAAI,yBAAyB,IAAI,KAAA,GAAW,eAAe,MAAM,CAAC;AAC3E"}
@@ -0,0 +1,35 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { getStore } from "../store.mjs";
5
+ import { requireSiteAdmin } from "./helpers.mjs";
6
+ import { hxRedirect } from "./htmx.mjs";
7
+ import { flash } from "./flash.mjs";
8
+ import { asString, findProject, renderSettingsPage } from "./settings.handlers.mjs";
9
+ import { ProjectModel } from "@storyshelf/core/models";
10
+ //#region src/routers/settings-ai.ts
11
+ /** Register the project AI settings tab (site admins set the profile gate). */
12
+ function registerAiSettings(app) {
13
+ app.get("/projects/:slug/settings/ai", async (c) => c.html(await renderSettingsPage(c, "ai")));
14
+ app.post("/projects/:slug/settings/ai", handleAiUpdate);
15
+ }
16
+ /** Error text for an unusable choice, or null when it is allowed. */
17
+ function profileError(ai, requested) {
18
+ if (!ai) return "AI is not configured";
19
+ return requested !== "" && !ai.profileNames().includes(requested) ? `Unknown profile "${requested}"` : null;
20
+ }
21
+ async function handleAiUpdate(c) {
22
+ requireSiteAdmin(c);
23
+ const { ai, db } = getStore();
24
+ const project = await findProject(c.req.param("slug") ?? "");
25
+ const requested = asString((await c.req.formData()).get("aiProfile")) ?? "";
26
+ const error = profileError(ai, requested);
27
+ if (error) return c.html(await renderSettingsPage(c, "ai", { globalError: error }), 400);
28
+ await new ProjectModel(db).update(project.id, { aiProfile: requested === "" ? null : requested });
29
+ flash(c, requested === "" ? "AI turned off for this project" : "AI profile saved");
30
+ return hxRedirect(c, `/projects/${project.slug}/settings/ai`);
31
+ }
32
+ //#endregion
33
+ export { registerAiSettings };
34
+
35
+ //# sourceMappingURL=settings-ai.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settings-ai.mjs","names":[],"sources":["../../src/routers/settings-ai.ts"],"sourcesContent":["import type { Ai } from \"@storyshelf/core/ai\";\nimport { ProjectModel } from \"@storyshelf/core/models\";\nimport type { Context } from \"hono\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { getStore } from \"../store.ts\";\nimport { flash } from \"./flash.ts\";\nimport { requireSiteAdmin } from \"./helpers.ts\";\nimport { hxRedirect } from \"./htmx.ts\";\nimport { asString, findProject, renderSettingsPage } from \"./settings.handlers.ts\";\n\n/** Register the project AI settings tab (site admins set the profile gate). */\nexport function registerAiSettings(app: ShelfRouter): void {\n app.get(\"/projects/:slug/settings/ai\", async (c) => c.html(await renderSettingsPage(c, \"ai\")));\n app.post(\"/projects/:slug/settings/ai\", handleAiUpdate);\n}\n\n/** Error text for an unusable choice, or null when it is allowed. */\nfunction profileError(ai: Ai | undefined, requested: string): string | null {\n if (!ai) {\n return \"AI is not configured\";\n }\n return requested !== \"\" && !ai.profileNames().includes(requested)\n ? `Unknown profile \"${requested}\"`\n : null;\n}\n\nasync function handleAiUpdate(c: Context): Promise<Response> {\n requireSiteAdmin(c);\n const { ai, db } = getStore();\n const project = await findProject(c.req.param(\"slug\") ?? \"\");\n const requested = asString((await c.req.formData()).get(\"aiProfile\")) ?? \"\";\n const error = profileError(ai, requested);\n if (error) {\n return c.html(await renderSettingsPage(c, \"ai\", { globalError: error }), 400);\n }\n await new ProjectModel(db).update(project.id, { aiProfile: requested === \"\" ? null : requested });\n flash(c, requested === \"\" ? \"AI turned off for this project\" : \"AI profile saved\");\n return hxRedirect(c, `/projects/${project.slug}/settings/ai`);\n}\n"],"mappings":";;;;;;;;;;;AAWA,SAAgB,mBAAmB,KAAwB;CACzD,IAAI,IAAI,+BAA+B,OAAO,MAAM,EAAE,KAAK,MAAM,mBAAmB,GAAG,IAAI,CAAC,CAAC;CAC7F,IAAI,KAAK,+BAA+B,cAAc;AACxD;;AAGA,SAAS,aAAa,IAAoB,WAAkC;CAC1E,IAAI,CAAC,IACH,OAAO;CAET,OAAO,cAAc,MAAM,CAAC,GAAG,aAAa,CAAC,CAAC,SAAS,SAAS,IAC5D,oBAAoB,UAAU,KAC9B;AACN;AAEA,eAAe,eAAe,GAA+B;CAC3D,iBAAiB,CAAC;CAClB,MAAM,EAAE,IAAI,OAAO,SAAS;CAC5B,MAAM,UAAU,MAAM,YAAY,EAAE,IAAI,MAAM,MAAM,KAAK,EAAE;CAC3D,MAAM,YAAY,UAAU,MAAM,EAAE,IAAI,SAAS,EAAA,CAAG,IAAI,WAAW,CAAC,KAAK;CACzE,MAAM,QAAQ,aAAa,IAAI,SAAS;CACxC,IAAI,OACF,OAAO,EAAE,KAAK,MAAM,mBAAmB,GAAG,MAAM,EAAE,aAAa,MAAM,CAAC,GAAG,GAAG;CAE9E,MAAM,IAAI,aAAa,EAAE,CAAC,CAAC,OAAO,QAAQ,IAAI,EAAE,WAAW,cAAc,KAAK,OAAO,UAAU,CAAC;CAChG,MAAM,GAAG,cAAc,KAAK,mCAAmC,kBAAkB;CACjF,OAAO,WAAW,GAAG,aAAa,QAAQ,KAAK,aAAa;AAC9D"}
@@ -0,0 +1,77 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { getStore } from "../store.mjs";
5
+ import { assertTargetsSelectable } from "../targets.mjs";
6
+ import { hxRedirect } from "./htmx.mjs";
7
+ import { flash } from "./flash.mjs";
8
+ import { toProjectPatch } from "./capture-config.mjs";
9
+ import { asString, findProject, renderSettingsPage } from "./settings.handlers.mjs";
10
+ import { storybookUrlTtlOverrideError } from "@storyshelf/core/utils";
11
+ import { resolveBrowsers } from "@storyshelf/core/capture";
12
+ import { ProjectModel } from "@storyshelf/core/models";
13
+ import { HTTPException } from "hono/http-exception";
14
+ import { isCaptureTarget } from "@storyshelf/core/adapter/capture-runner";
15
+ //#region src/routers/settings-capture.ts
16
+ /** Capture settings: browsers (primary + extras), the mode allowlist and the signed URL lifetime. */
17
+ function registerCaptureSettings(app) {
18
+ app.get("/projects/:slug/settings/capture", async (c) => c.html(await renderSettingsPage(c, "capture")));
19
+ app.post("/projects/:slug/settings/capture", handleCaptureUpdate);
20
+ }
21
+ const EXTRA_PREFIX = "extra_";
22
+ /** Primary target first, then the ticked extras; throws 400 when the form is malformed. */
23
+ function readBrowsers(form) {
24
+ const primary = asString(form.get("browser")) ?? "chromium";
25
+ if (!isCaptureTarget(primary)) throw badBrowser(primary);
26
+ const extras = [...form.entries()].flatMap(([key, value]) => key.startsWith(EXTRA_PREFIX) && value === key.slice(6) ? [key.slice(6)] : []);
27
+ const bad = extras.find((extra) => !isCaptureTarget(extra));
28
+ if (bad !== void 0) throw badBrowser(bad);
29
+ return [.../* @__PURE__ */ new Set([primary, ...extras])];
30
+ }
31
+ function badBrowser(value) {
32
+ return new HTTPException(400, { message: `Unknown browser "${value}"` });
33
+ }
34
+ /** Mode names, one per line (blank lines and surrounding space ignored). */
35
+ function readAllowlist(form) {
36
+ const lines = (asString(form.get("modesAllowlist")) ?? "").split("\n").map((line) => line.trim()).filter((line) => line !== "");
37
+ return lines.length > 0 ? lines : null;
38
+ }
39
+ /** Seconds of the signed Storybook URL lifetime; empty derives it from the stories (null). */
40
+ function readUrlTtl(form) {
41
+ const raw = asString(form.get("storybookUrlTtlSeconds"));
42
+ if (!raw) return null;
43
+ const seconds = Number(raw);
44
+ const message = storybookUrlTtlOverrideError(seconds);
45
+ if (message !== void 0) throw new HTTPException(400, { message });
46
+ return seconds;
47
+ }
48
+ async function reject(c, message) {
49
+ return c.html(await renderSettingsPage(c, "capture", { globalError: message }) ?? "", 400);
50
+ }
51
+ async function saveCapture(project, form, browsers) {
52
+ const patch = toProjectPatch({
53
+ browsers: browsers.length > 1 ? browsers : null,
54
+ browser: browsers[0],
55
+ modesAllowlist: readAllowlist(form),
56
+ storybookUrlTtlSeconds: readUrlTtl(form)
57
+ });
58
+ await new ProjectModel(getStore().db).update(project.id, patch);
59
+ }
60
+ async function handleCaptureUpdate(c) {
61
+ const project = await findProject(c.req.param("slug") ?? "");
62
+ const form = await c.req.formData();
63
+ try {
64
+ const browsers = readBrowsers(form);
65
+ await assertTargetsSelectable(browsers, resolveBrowsers(project));
66
+ await saveCapture(project, form, browsers);
67
+ } catch (error) {
68
+ if (error instanceof HTTPException) return await reject(c, error.message);
69
+ throw error;
70
+ }
71
+ flash(c, "Capture settings saved");
72
+ return hxRedirect(c, `/projects/${project.slug}/settings/capture`);
73
+ }
74
+ //#endregion
75
+ export { registerCaptureSettings };
76
+
77
+ //# sourceMappingURL=settings-capture.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settings-capture.mjs","names":[],"sources":["../../src/routers/settings-capture.ts"],"sourcesContent":["import { isCaptureTarget, type CaptureTarget } from \"@storyshelf/core/adapter/capture-runner\";\nimport { resolveBrowsers } from \"@storyshelf/core/capture\";\nimport { ProjectModel } from \"@storyshelf/core/models\";\nimport type { Project } from \"@storyshelf/core/schema\";\nimport { storybookUrlTtlOverrideError } from \"@storyshelf/core/utils\";\nimport type { Context } from \"hono\";\nimport { HTTPException } from \"hono/http-exception\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { getStore } from \"../store.ts\";\nimport { assertTargetsSelectable } from \"../targets.ts\";\nimport { toProjectPatch } from \"./capture-config.ts\";\nimport { flash } from \"./flash.ts\";\nimport { hxRedirect } from \"./htmx.ts\";\nimport { asString, findProject, renderSettingsPage } from \"./settings.handlers.ts\";\n\n/** Capture settings: browsers (primary + extras), the mode allowlist and the signed URL lifetime. */\nexport function registerCaptureSettings(app: ShelfRouter): void {\n app.get(\"/projects/:slug/settings/capture\", async (c) =>\n c.html(await renderSettingsPage(c, \"capture\")),\n );\n app.post(\"/projects/:slug/settings/capture\", handleCaptureUpdate);\n}\n\nconst EXTRA_PREFIX = \"extra_\";\n\n/** Primary target first, then the ticked extras; throws 400 when the form is malformed. */\nfunction readBrowsers(form: FormData): CaptureTarget[] {\n const primary = asString(form.get(\"browser\")) ?? \"chromium\";\n if (!isCaptureTarget(primary)) throw badBrowser(primary);\n const extras = [...form.entries()].flatMap(([key, value]) =>\n key.startsWith(EXTRA_PREFIX) && value === key.slice(EXTRA_PREFIX.length)\n ? [key.slice(EXTRA_PREFIX.length)]\n : [],\n );\n const bad = extras.find((extra) => !isCaptureTarget(extra));\n if (bad !== undefined) throw badBrowser(bad);\n return [...new Set([primary, ...(extras as CaptureTarget[])])];\n}\n\nfunction badBrowser(value: string): HTTPException {\n return new HTTPException(400, { message: `Unknown browser \"${value}\"` });\n}\n\n/** Mode names, one per line (blank lines and surrounding space ignored). */\nfunction readAllowlist(form: FormData): string[] | null {\n const lines = (asString(form.get(\"modesAllowlist\")) ?? \"\")\n .split(\"\\n\")\n .map((line) => line.trim())\n .filter((line) => line !== \"\");\n return lines.length > 0 ? lines : null;\n}\n\n/** Seconds of the signed Storybook URL lifetime; empty derives it from the stories (null). */\nfunction readUrlTtl(form: FormData): number | null {\n const raw = asString(form.get(\"storybookUrlTtlSeconds\"));\n if (!raw) return null;\n const seconds = Number(raw);\n const message = storybookUrlTtlOverrideError(seconds);\n if (message !== undefined) {\n throw new HTTPException(400, { message });\n }\n return seconds;\n}\n\nasync function reject(c: Context, message: string): Promise<Response> {\n return c.html((await renderSettingsPage(c, \"capture\", { globalError: message })) ?? \"\", 400);\n}\n\nasync function saveCapture(\n project: Project,\n form: FormData,\n browsers: CaptureTarget[],\n): Promise<void> {\n const patch = toProjectPatch({\n browsers: browsers.length > 1 ? browsers : null,\n browser: browsers[0],\n modesAllowlist: readAllowlist(form),\n storybookUrlTtlSeconds: readUrlTtl(form),\n });\n await new ProjectModel(getStore().db).update(project.id, patch);\n}\n\nasync function handleCaptureUpdate(c: Context): Promise<Response> {\n const project = await findProject(c.req.param(\"slug\") ?? \"\");\n const form = await c.req.formData();\n try {\n const browsers = readBrowsers(form);\n await assertTargetsSelectable(browsers, resolveBrowsers(project));\n await saveCapture(project, form, browsers);\n } catch (error) {\n if (error instanceof HTTPException) return await reject(c, error.message);\n throw error;\n }\n flash(c, \"Capture settings saved\");\n return hxRedirect(c, `/projects/${project.slug}/settings/capture`);\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAgBA,SAAgB,wBAAwB,KAAwB;CAC9D,IAAI,IAAI,oCAAoC,OAAO,MACjD,EAAE,KAAK,MAAM,mBAAmB,GAAG,SAAS,CAAC,CAC/C;CACA,IAAI,KAAK,oCAAoC,mBAAmB;AAClE;AAEA,MAAM,eAAe;;AAGrB,SAAS,aAAa,MAAiC;CACrD,MAAM,UAAU,SAAS,KAAK,IAAI,SAAS,CAAC,KAAK;CACjD,IAAI,CAAC,gBAAgB,OAAO,GAAG,MAAM,WAAW,OAAO;CACvD,MAAM,SAAS,CAAC,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,WAChD,IAAI,WAAW,YAAY,KAAK,UAAU,IAAI,MAAM,CAAmB,IACnE,CAAC,IAAI,MAAM,CAAmB,CAAC,IAC/B,CAAC,CACP;CACA,MAAM,MAAM,OAAO,MAAM,UAAU,CAAC,gBAAgB,KAAK,CAAC;CAC1D,IAAI,QAAQ,KAAA,GAAW,MAAM,WAAW,GAAG;CAC3C,OAAO,CAAC,mBAAG,IAAI,IAAI,CAAC,SAAS,GAAI,MAA0B,CAAC,CAAC;AAC/D;AAEA,SAAS,WAAW,OAA8B;CAChD,OAAO,IAAI,cAAc,KAAK,EAAE,SAAS,oBAAoB,MAAM,GAAG,CAAC;AACzE;;AAGA,SAAS,cAAc,MAAiC;CACtD,MAAM,SAAS,SAAS,KAAK,IAAI,gBAAgB,CAAC,KAAK,GAAA,CACpD,MAAM,IAAI,CAAC,CACX,KAAK,SAAS,KAAK,KAAK,CAAC,CAAC,CAC1B,QAAQ,SAAS,SAAS,EAAE;CAC/B,OAAO,MAAM,SAAS,IAAI,QAAQ;AACpC;;AAGA,SAAS,WAAW,MAA+B;CACjD,MAAM,MAAM,SAAS,KAAK,IAAI,wBAAwB,CAAC;CACvD,IAAI,CAAC,KAAK,OAAO;CACjB,MAAM,UAAU,OAAO,GAAG;CAC1B,MAAM,UAAU,6BAA6B,OAAO;CACpD,IAAI,YAAY,KAAA,GACd,MAAM,IAAI,cAAc,KAAK,EAAE,QAAQ,CAAC;CAE1C,OAAO;AACT;AAEA,eAAe,OAAO,GAAY,SAAoC;CACpE,OAAO,EAAE,KAAM,MAAM,mBAAmB,GAAG,WAAW,EAAE,aAAa,QAAQ,CAAC,KAAM,IAAI,GAAG;AAC7F;AAEA,eAAe,YACb,SACA,MACA,UACe;CACf,MAAM,QAAQ,eAAe;EAC3B,UAAU,SAAS,SAAS,IAAI,WAAW;EAC3C,SAAS,SAAS;EAClB,gBAAgB,cAAc,IAAI;EAClC,wBAAwB,WAAW,IAAI;CACzC,CAAC;CACD,MAAM,IAAI,aAAa,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,OAAO,QAAQ,IAAI,KAAK;AAChE;AAEA,eAAe,oBAAoB,GAA+B;CAChE,MAAM,UAAU,MAAM,YAAY,EAAE,IAAI,MAAM,MAAM,KAAK,EAAE;CAC3D,MAAM,OAAO,MAAM,EAAE,IAAI,SAAS;CAClC,IAAI;EACF,MAAM,WAAW,aAAa,IAAI;EAClC,MAAM,wBAAwB,UAAU,gBAAgB,OAAO,CAAC;EAChE,MAAM,YAAY,SAAS,MAAM,QAAQ;CAC3C,SAAS,OAAO;EACd,IAAI,iBAAiB,eAAe,OAAO,MAAM,OAAO,GAAG,MAAM,OAAO;EACxE,MAAM;CACR;CACA,MAAM,GAAG,wBAAwB;CACjC,OAAO,WAAW,GAAG,aAAa,QAAQ,KAAK,kBAAkB;AACnE"}
@@ -0,0 +1,103 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { credentialKeys } from "../credential-keys.mjs";
5
+ import { getStore } from "../store.mjs";
6
+ import { notFound, resolveAuthorizedProject } from "./helpers.mjs";
7
+ import { hxRedirect } from "./htmx.mjs";
8
+ import { flash } from "./flash.mjs";
9
+ import { asString, renderSettingsPage } from "./settings.handlers.mjs";
10
+ import { DesignCredentialModel, DesignModel } from "@storyshelf/core/models";
11
+ import { InvalidDesignUrlError } from "@storyshelf/core/adapter/design";
12
+ //#region src/routers/settings-design.ts
13
+ /** Design settings (project design files, component mapping, provider credentials). */
14
+ function registerDesignSettings(app) {
15
+ const base = "/projects/:slug/settings/design";
16
+ app.get(base, async (c) => c.html(await renderSettingsPage(c, "design")));
17
+ app.post(`${base}/files`, handleAddFile);
18
+ app.post(`${base}/files/:id/delete`, handleRemoveLink);
19
+ app.post(`${base}/components`, handleMapComponent);
20
+ app.post(`${base}/components/:id/delete`, handleRemoveLink);
21
+ app.post(`${base}/:provider/token`, handleSetToken);
22
+ app.post(`${base}/:provider/token/delete`, handleRemoveToken);
23
+ }
24
+ /**
25
+ * Every design-settings mutation writes credentials or mappings: require a
26
+ * project admin server-side (the forms are only hidden from others, never trusted).
27
+ * With auth disabled the app has no roles and everyone may administer, as elsewhere.
28
+ */
29
+ async function adminProject(c) {
30
+ return await resolveAuthorizedProject(c, c.req.param("slug") ?? "", "admin");
31
+ }
32
+ function designModel() {
33
+ const { db, design } = getStore();
34
+ return new DesignModel(db, design);
35
+ }
36
+ function credentialModel() {
37
+ const { db, config } = getStore();
38
+ return new DesignCredentialModel(db, credentialKeys(config));
39
+ }
40
+ /** The configured provider addressed by `:provider`, or a 404. */
41
+ function providerOf(c) {
42
+ const provider = getStore().design.find((candidate) => candidate.metadata.kind === c.req.param("provider"));
43
+ if (!provider) notFound("Unknown design provider");
44
+ return provider;
45
+ }
46
+ async function reject(c, errors) {
47
+ return c.html(await renderSettingsPage(c, "design", { errors }) ?? "", 400);
48
+ }
49
+ function done(c, slug, message) {
50
+ flash(c, message);
51
+ return hxRedirect(c, `/projects/${slug}/settings/design`);
52
+ }
53
+ async function handleAddFile(c) {
54
+ const project = await adminProject(c);
55
+ const form = await c.req.formData();
56
+ const label = asString(form.get("label")) ?? "";
57
+ try {
58
+ await designModel().addProjectLink(project.id, {
59
+ label,
60
+ url: asString(form.get("url")) ?? ""
61
+ });
62
+ } catch (error) {
63
+ if (error instanceof InvalidDesignUrlError) return await reject(c, { file: error.message });
64
+ throw error;
65
+ }
66
+ return done(c, project.slug, "Design file added");
67
+ }
68
+ async function handleMapComponent(c) {
69
+ const project = await adminProject(c);
70
+ const form = await c.req.formData();
71
+ const importPath = asString(form.get("importPath")) ?? "";
72
+ if (!importPath) return await reject(c, { component: "Enter the story file path" });
73
+ try {
74
+ await designModel().setComponentLink(project.id, importPath, asString(form.get("url")) ?? "");
75
+ } catch (error) {
76
+ if (error instanceof InvalidDesignUrlError) return await reject(c, { component: error.message });
77
+ throw error;
78
+ }
79
+ return done(c, project.slug, "Component mapped");
80
+ }
81
+ async function handleRemoveLink(c) {
82
+ const project = await adminProject(c);
83
+ await designModel().removeLink(project.id, c.req.param("id") ?? "");
84
+ return done(c, project.slug, "Link removed");
85
+ }
86
+ async function handleSetToken(c) {
87
+ const project = await adminProject(c);
88
+ const provider = providerOf(c);
89
+ const token = asString((await c.req.formData()).get("token"));
90
+ if (!token) return await reject(c, { [`token:${provider.metadata.kind}`]: "Token is required" });
91
+ await credentialModel().set(project.id, provider.metadata.kind, token);
92
+ return done(c, project.slug, `${provider.metadata.name} token saved`);
93
+ }
94
+ async function handleRemoveToken(c) {
95
+ const project = await adminProject(c);
96
+ const provider = providerOf(c);
97
+ await credentialModel().remove(project.id, provider.metadata.kind);
98
+ return done(c, project.slug, `${provider.metadata.name} token removed`);
99
+ }
100
+ //#endregion
101
+ export { registerDesignSettings };
102
+
103
+ //# sourceMappingURL=settings-design.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settings-design.mjs","names":[],"sources":["../../src/routers/settings-design.ts"],"sourcesContent":["import { InvalidDesignUrlError, type DesignProvider } from \"@storyshelf/core/adapter/design\";\nimport { DesignCredentialModel, DesignModel } from \"@storyshelf/core/models\";\nimport type { Project } from \"@storyshelf/core/schema\";\nimport type { Context } from \"hono\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { credentialKeys } from \"../credential-keys.ts\";\nimport { getStore } from \"../store.ts\";\nimport { flash } from \"./flash.ts\";\nimport { notFound, resolveAuthorizedProject } from \"./helpers.ts\";\nimport { hxRedirect } from \"./htmx.ts\";\nimport { asString, renderSettingsPage } from \"./settings.handlers.ts\";\n\n/** Design settings (project design files, component mapping, provider credentials). */\nexport function registerDesignSettings(app: ShelfRouter): void {\n const base = \"/projects/:slug/settings/design\";\n app.get(base, async (c) => c.html(await renderSettingsPage(c, \"design\")));\n app.post(`${base}/files`, handleAddFile);\n app.post(`${base}/files/:id/delete`, handleRemoveLink);\n app.post(`${base}/components`, handleMapComponent);\n app.post(`${base}/components/:id/delete`, handleRemoveLink);\n app.post(`${base}/:provider/token`, handleSetToken);\n app.post(`${base}/:provider/token/delete`, handleRemoveToken);\n}\n\n/**\n * Every design-settings mutation writes credentials or mappings: require a\n * project admin server-side (the forms are only hidden from others, never trusted).\n * With auth disabled the app has no roles and everyone may administer, as elsewhere.\n */\nasync function adminProject(c: Context): Promise<Project> {\n return await resolveAuthorizedProject(c, c.req.param(\"slug\") ?? \"\", \"admin\");\n}\n\nfunction designModel(): DesignModel {\n const { db, design } = getStore();\n return new DesignModel(db, design);\n}\n\nfunction credentialModel(): DesignCredentialModel {\n const { db, config } = getStore();\n return new DesignCredentialModel(db, credentialKeys(config));\n}\n\n/** The configured provider addressed by `:provider`, or a 404. */\nfunction providerOf(c: Context): DesignProvider {\n const provider = getStore().design.find(\n (candidate) => candidate.metadata.kind === c.req.param(\"provider\"),\n );\n if (!provider) notFound(\"Unknown design provider\");\n return provider;\n}\n\nasync function reject(c: Context, errors: Record<string, string>): Promise<Response> {\n return c.html((await renderSettingsPage(c, \"design\", { errors })) ?? \"\", 400);\n}\n\nfunction done(c: Context, slug: string, message: string): Response {\n flash(c, message);\n return hxRedirect(c, `/projects/${slug}/settings/design`);\n}\n\nasync function handleAddFile(c: Context): Promise<Response> {\n const project = await adminProject(c);\n const form = await c.req.formData();\n const label = asString(form.get(\"label\")) ?? \"\";\n try {\n await designModel().addProjectLink(project.id, { label, url: asString(form.get(\"url\")) ?? \"\" });\n } catch (error) {\n if (error instanceof InvalidDesignUrlError) return await reject(c, { file: error.message });\n throw error;\n }\n return done(c, project.slug, \"Design file added\");\n}\n\nasync function handleMapComponent(c: Context): Promise<Response> {\n const project = await adminProject(c);\n const form = await c.req.formData();\n const importPath = asString(form.get(\"importPath\")) ?? \"\";\n if (!importPath) return await reject(c, { component: \"Enter the story file path\" });\n try {\n await designModel().setComponentLink(project.id, importPath, asString(form.get(\"url\")) ?? \"\");\n } catch (error) {\n if (error instanceof InvalidDesignUrlError) {\n return await reject(c, { component: error.message });\n }\n throw error;\n }\n return done(c, project.slug, \"Component mapped\");\n}\n\nasync function handleRemoveLink(c: Context): Promise<Response> {\n const project = await adminProject(c);\n await designModel().removeLink(project.id, c.req.param(\"id\") ?? \"\");\n return done(c, project.slug, \"Link removed\");\n}\n\nasync function handleSetToken(c: Context): Promise<Response> {\n const project = await adminProject(c);\n const provider = providerOf(c);\n const token = asString((await c.req.formData()).get(\"token\"));\n if (!token) return await reject(c, { [`token:${provider.metadata.kind}`]: \"Token is required\" });\n await credentialModel().set(project.id, provider.metadata.kind, token);\n return done(c, project.slug, `${provider.metadata.name} token saved`);\n}\n\nasync function handleRemoveToken(c: Context): Promise<Response> {\n const project = await adminProject(c);\n const provider = providerOf(c);\n await credentialModel().remove(project.id, provider.metadata.kind);\n return done(c, project.slug, `${provider.metadata.name} token removed`);\n}\n"],"mappings":"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"}
@@ -2,11 +2,14 @@ import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
4
  import { getStore } from "../store.mjs";
5
+ import { resolveAuthorizedProject } from "./helpers.mjs";
6
+ import { assertTargetsSelectable } from "../targets.mjs";
5
7
  import { hxRedirect } from "./htmx.mjs";
6
8
  import { flash } from "./flash.mjs";
9
+ import { runSwitchOffPass } from "./review-switch-off.mjs";
7
10
  import { asString, findProject, renderSettingsPage } from "./settings.handlers.mjs";
11
+ import { resolveBrowsers } from "@storyshelf/core/capture";
8
12
  import { ProjectModel } from "@storyshelf/core/models";
9
- import { BROWSER_NAMES } from "@storyshelf/core/adapter/capture-runner";
10
13
  //#region src/routers/settings-general.ts
11
14
  /** General project settings (name, repo, thresholds) and danger-zone delete. */
12
15
  function registerGeneralSettings(app) {
@@ -14,6 +17,7 @@ function registerGeneralSettings(app) {
14
17
  app.get("/projects/:slug/settings/tests", async (c) => c.html(await renderSettingsPage(c, "tests")));
15
18
  app.post("/projects/:slug/settings", handleGeneralUpdate);
16
19
  app.post("/projects/:slug/settings/tests", handleTestsUpdate);
20
+ app.post("/projects/:slug/settings/review", handleReviewUpdate);
17
21
  app.post("/projects/:slug/delete", handleDeleteProject);
18
22
  }
19
23
  /** Read the general-settings form fields (name validated by the caller). */
@@ -33,15 +37,18 @@ async function handleGeneralUpdate(c) {
33
37
  const form = await c.req.formData();
34
38
  const name = form.get("name");
35
39
  if (typeof name !== "string" || name.trim() === "") return c.html(await renderSettingsPage(c, "general", { errors: { name: "Name is required" } }) ?? "", 400);
36
- const fields = {
40
+ return await persistGeneralUpdate(c, project, slug, {
37
41
  ...readGeneralFields(form),
38
42
  name: name.trim()
39
- };
40
- if (fields.browser !== void 0 && !BROWSER_NAMES.includes(fields.browser)) return c.html(await renderSettingsPage(c, "general", { globalError: `Invalid browser "${fields.browser}". Choose one of: ${BROWSER_NAMES.join(", ")}.` }) ?? "", 400);
41
- return await persistGeneralUpdate(c, project, slug, fields);
43
+ });
44
+ }
45
+ /** Refuse, with 400 and the reason, a browser the runner cannot render (ADR 0007, decision 7). */
46
+ async function assertBrowserChoice(project, browser) {
47
+ if (browser !== void 0) await assertTargetsSelectable([browser], resolveBrowsers(project));
42
48
  }
43
49
  async function persistGeneralUpdate(c, project, slug, fields) {
44
50
  try {
51
+ await assertBrowserChoice(project, fields.browser);
45
52
  await new ProjectModel(getStore().db).update(project.id, {
46
53
  name: fields.name,
47
54
  gitRepository: fields.gitRepository ?? void 0,
@@ -49,7 +56,8 @@ async function persistGeneralUpdate(c, project, slug, fields) {
49
56
  pixelThreshold: fields.pixelThreshold ? Number(fields.pixelThreshold) : void 0,
50
57
  maxDiffRatio: fields.maxDiffRatio ? Number(fields.maxDiffRatio) : void 0,
51
58
  publicBranchRegex: fields.publicBranchRegex === "" ? null : fields.publicBranchRegex ?? void 0,
52
- browser: fields.browser ?? void 0
59
+ browser: fields.browser ?? void 0,
60
+ ...fields.browser && fields.browser !== project.browser ? { browsers: null } : {}
53
61
  });
54
62
  flash(c, "Project settings saved");
55
63
  return hxRedirect(c, `/projects/${slug}/settings`);
@@ -61,16 +69,22 @@ async function persistGeneralUpdate(c, project, slug, fields) {
61
69
  async function handleTestsUpdate(c) {
62
70
  const slug = c.req.param("slug") ?? "";
63
71
  const project = await findProject(slug);
64
- const { executePlay, playTimeoutMs } = readTestsFields(await c.req.formData());
72
+ const { executePlay, runA11y, runTests, playTimeoutMs } = readTestsFields(await c.req.formData());
65
73
  const rangeError = playTimeoutError(playTimeoutMs);
66
74
  if (rangeError) return c.html(await renderSettingsPage(c, "tests", { globalError: rangeError }) ?? "", 400);
67
- return await persistTestsUpdate(c, project, slug, executePlay, playTimeoutMs);
75
+ return await persistTestsUpdate(c, project, slug, {
76
+ executePlay,
77
+ runA11y,
78
+ runTests
79
+ }, playTimeoutMs);
68
80
  }
69
81
  /** Read the tests-tab form fields. */
70
82
  function readTestsFields(form) {
71
83
  const playTimeoutMsRaw = asString(form.get("playTimeoutMs"));
72
84
  return {
73
85
  executePlay: form.get("executePlay") === "true",
86
+ runA11y: form.get("runA11y") === "true",
87
+ runTests: form.get("runTests") === "true",
74
88
  playTimeoutMs: playTimeoutMsRaw ? Number(playTimeoutMsRaw) : void 0
75
89
  };
76
90
  }
@@ -79,10 +93,10 @@ function playTimeoutError(playTimeoutMs) {
79
93
  if (playTimeoutMs !== void 0 && (Number.isNaN(playTimeoutMs) || playTimeoutMs < 1e3 || playTimeoutMs > 3e4)) return "Play timeout must be between 1000 and 30000";
80
94
  return null;
81
95
  }
82
- async function persistTestsUpdate(c, project, slug, executePlay, playTimeoutMs) {
96
+ async function persistTestsUpdate(c, project, slug, switches, playTimeoutMs) {
83
97
  try {
84
98
  await new ProjectModel(getStore().db).update(project.id, {
85
- executePlay,
99
+ ...switches,
86
100
  playTimeoutMs: playTimeoutMs ?? void 0
87
101
  });
88
102
  flash(c, "Test settings saved");
@@ -98,6 +112,37 @@ async function handleDeleteProject(c) {
98
112
  flash(c, `Deleted ${project.name}`);
99
113
  return hxRedirect(c, "/projects");
100
114
  }
115
+ /** Largest approval count the form and the API accept. */
116
+ const MAX_APPROVALS = 20;
117
+ /**
118
+ * Review settings (ADR 0013, decision 1): the switch and the approvals a build needs. Project
119
+ * admins only. The route is classed `hosting`, not `review`: a review route answers 404 while
120
+ * review is off, and this is the form that turns it back on. Turning review off accepts the
121
+ * project's pending builds that no capture worked on, in one pass.
122
+ */
123
+ async function handleReviewUpdate(c) {
124
+ const slug = c.req.param("slug") ?? "";
125
+ const project = await resolveAuthorizedProject(c, slug, "admin");
126
+ const form = await c.req.formData();
127
+ const approvals = approvalsOf(form, project);
128
+ if (approvals === null) return c.html(await renderSettingsPage(c, "general", { errors: { reviewApprovals: `Enter a whole number from 1 to ${MAX_APPROVALS}` } }) ?? "", 400);
129
+ const updated = await new ProjectModel(getStore().db).update(project.id, {
130
+ reviewEnabled: form.get("reviewEnabled") === "true",
131
+ reviewApprovals: approvals
132
+ });
133
+ const accepted = await runSwitchOffPass(project, updated);
134
+ flash(c, reviewSavedMessage(accepted));
135
+ return hxRedirect(c, `/projects/${slug}/settings`);
136
+ }
137
+ /** The approvals count of the form (the current one when blank); null when it is not 1 to 20. */
138
+ function approvalsOf(form, project) {
139
+ const raw = asString(form.get("reviewApprovals"));
140
+ const approvals = raw ? Number(raw) : project.reviewApprovals ?? 1;
141
+ return Number.isInteger(approvals) && approvals >= 1 && approvals <= MAX_APPROVALS ? approvals : null;
142
+ }
143
+ function reviewSavedMessage(accepted) {
144
+ return accepted > 0 ? `Review settings saved; accepted ${accepted} pending build${accepted === 1 ? "" : "s"}` : "Review settings saved";
145
+ }
101
146
  //#endregion
102
147
  export { registerGeneralSettings };
103
148
 
@@ -1 +1 @@
1
- {"version":3,"file":"settings-general.mjs","names":[],"sources":["../../src/routers/settings-general.ts"],"sourcesContent":["import { BROWSER_NAMES } from \"@storyshelf/core/adapter/capture-runner\";\nimport { ProjectModel } from \"@storyshelf/core/models\";\nimport type { Project } from \"@storyshelf/core/schema\";\nimport type { Context } from \"hono\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { getStore } from \"../store.ts\";\nimport { flash } from \"./flash.ts\";\nimport { hxRedirect } from \"./htmx.ts\";\nimport { asString, findProject, renderSettingsPage } from \"./settings.handlers.ts\";\n/** General project settings (name, repo, thresholds) and danger-zone delete. */\nexport function registerGeneralSettings(app: ShelfRouter): void {\n app.get(\"/projects/:slug/settings\", async (c) => c.html(await renderSettingsPage(c, \"general\")));\n app.get(\"/projects/:slug/settings/tests\", async (c) =>\n c.html(await renderSettingsPage(c, \"tests\")),\n );\n app.post(\"/projects/:slug/settings\", handleGeneralUpdate);\n app.post(\"/projects/:slug/settings/tests\", handleTestsUpdate);\n app.post(\"/projects/:slug/delete\", handleDeleteProject);\n}\n\ninterface GeneralFields {\n name: string;\n gitRepository?: string;\n gitDefaultBranch?: string;\n pixelThreshold?: string;\n maxDiffRatio?: string;\n publicBranchRegex?: string;\n browser?: string;\n}\n\n/** Read the general-settings form fields (name validated by the caller). */\nfunction readGeneralFields(form: FormData): Omit<GeneralFields, \"name\"> {\n return {\n gitRepository: asString(form.get(\"gitRepository\")),\n gitDefaultBranch: asString(form.get(\"gitDefaultBranch\")),\n pixelThreshold: asString(form.get(\"pixelThreshold\")),\n maxDiffRatio: asString(form.get(\"maxDiffRatio\")),\n publicBranchRegex: asString(form.get(\"publicBranchRegex\")),\n browser: asString(form.get(\"browser\")),\n };\n}\n\nasync function handleGeneralUpdate(c: Context): Promise<Response> {\n const slug = c.req.param(\"slug\") ?? \"\";\n const project = await findProject(slug);\n const form = await c.req.formData();\n const name = form.get(\"name\");\n if (typeof name !== \"string\" || name.trim() === \"\") {\n return c.html(\n (await renderSettingsPage(c, \"general\", { errors: { name: \"Name is required\" } })) ?? \"\",\n 400,\n );\n }\n const fields: GeneralFields = { ...readGeneralFields(form), name: name.trim() };\n if (\n fields.browser !== undefined &&\n !(BROWSER_NAMES as readonly string[]).includes(fields.browser)\n ) {\n return c.html(\n (await renderSettingsPage(c, \"general\", {\n globalError: `Invalid browser \"${fields.browser}\". Choose one of: ${BROWSER_NAMES.join(\", \")}.`,\n })) ?? \"\",\n 400,\n );\n }\n return await persistGeneralUpdate(c, project, slug, fields);\n}\n\nasync function persistGeneralUpdate(\n c: Context,\n project: Project,\n slug: string,\n fields: GeneralFields,\n): Promise<Response> {\n try {\n await new ProjectModel(getStore().db).update(project.id, {\n name: fields.name,\n gitRepository: fields.gitRepository ?? undefined,\n gitDefaultBranch: fields.gitDefaultBranch ?? undefined,\n pixelThreshold: fields.pixelThreshold ? Number(fields.pixelThreshold) : undefined,\n maxDiffRatio: fields.maxDiffRatio ? Number(fields.maxDiffRatio) : undefined,\n publicBranchRegex:\n fields.publicBranchRegex === \"\" ? null : (fields.publicBranchRegex ?? undefined),\n browser: fields.browser ?? undefined,\n });\n flash(c, \"Project settings saved\");\n return hxRedirect(c, `/projects/${slug}/settings`);\n } catch (error) {\n const message = error instanceof Error ? error.message : \"Failed to update\";\n return c.html((await renderSettingsPage(c, \"general\", { globalError: message })) ?? \"\", 400);\n }\n}\n\nasync function handleTestsUpdate(c: Context): Promise<Response> {\n const slug = c.req.param(\"slug\") ?? \"\";\n const project = await findProject(slug);\n const form = await c.req.formData();\n const { executePlay, playTimeoutMs } = readTestsFields(form);\n const rangeError = playTimeoutError(playTimeoutMs);\n if (rangeError) {\n return c.html((await renderSettingsPage(c, \"tests\", { globalError: rangeError })) ?? \"\", 400);\n }\n return await persistTestsUpdate(c, project, slug, executePlay, playTimeoutMs);\n}\n\n/** Read the tests-tab form fields. */\nfunction readTestsFields(form: FormData): {\n executePlay: boolean;\n playTimeoutMs: number | undefined;\n} {\n const playTimeoutMsRaw = asString(form.get(\"playTimeoutMs\"));\n return {\n executePlay: form.get(\"executePlay\") === \"true\",\n playTimeoutMs: playTimeoutMsRaw ? Number(playTimeoutMsRaw) : undefined,\n };\n}\n\n/** Range error for the play timeout, or null when valid. */\nfunction playTimeoutError(playTimeoutMs: number | undefined): string | null {\n if (\n playTimeoutMs !== undefined &&\n (Number.isNaN(playTimeoutMs) || playTimeoutMs < 1000 || playTimeoutMs > 30_000)\n ) {\n return \"Play timeout must be between 1000 and 30000\";\n }\n return null;\n}\n\nasync function persistTestsUpdate(\n c: Context,\n project: Project,\n slug: string,\n executePlay: boolean,\n playTimeoutMs: number | undefined,\n): Promise<Response> {\n try {\n await new ProjectModel(getStore().db).update(project.id, {\n executePlay,\n playTimeoutMs: playTimeoutMs ?? undefined,\n });\n flash(c, \"Test settings saved\");\n return hxRedirect(c, `/projects/${slug}/settings/tests`);\n } catch (error) {\n const message = error instanceof Error ? error.message : \"Failed to update\";\n return c.html((await renderSettingsPage(c, \"tests\", { globalError: message })) ?? \"\", 400);\n }\n}\n\nasync function handleDeleteProject(c: Context): Promise<Response> {\n const project = await findProject(c.req.param(\"slug\") ?? \"\");\n await new ProjectModel(getStore().db).remove(project.id);\n flash(c, `Deleted ${project.name}`);\n return hxRedirect(c, \"/projects\");\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"settings-general.mjs","names":[],"sources":["../../src/routers/settings-general.ts"],"sourcesContent":["import { resolveBrowsers } from \"@storyshelf/core/capture\";\nimport { ProjectModel } from \"@storyshelf/core/models\";\nimport type { Project } from \"@storyshelf/core/schema\";\nimport type { Context } from \"hono\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { getStore } from \"../store.ts\";\nimport { assertTargetsSelectable } from \"../targets.ts\";\nimport { flash } from \"./flash.ts\";\nimport { resolveAuthorizedProject } from \"./helpers.ts\";\nimport { hxRedirect } from \"./htmx.ts\";\nimport { runSwitchOffPass } from \"./review-switch-off.ts\";\nimport { asString, findProject, renderSettingsPage } from \"./settings.handlers.ts\";\n/** General project settings (name, repo, thresholds) and danger-zone delete. */\nexport function registerGeneralSettings(app: ShelfRouter): void {\n app.get(\"/projects/:slug/settings\", async (c) => c.html(await renderSettingsPage(c, \"general\")));\n app.get(\"/projects/:slug/settings/tests\", async (c) =>\n c.html(await renderSettingsPage(c, \"tests\")),\n );\n app.post(\"/projects/:slug/settings\", handleGeneralUpdate);\n app.post(\"/projects/:slug/settings/tests\", handleTestsUpdate);\n app.post(\"/projects/:slug/settings/review\", handleReviewUpdate);\n app.post(\"/projects/:slug/delete\", handleDeleteProject);\n}\n\ninterface GeneralFields {\n name: string;\n gitRepository?: string;\n gitDefaultBranch?: string;\n pixelThreshold?: string;\n maxDiffRatio?: string;\n publicBranchRegex?: string;\n browser?: string;\n}\n\n/** Read the general-settings form fields (name validated by the caller). */\nfunction readGeneralFields(form: FormData): Omit<GeneralFields, \"name\"> {\n return {\n gitRepository: asString(form.get(\"gitRepository\")),\n gitDefaultBranch: asString(form.get(\"gitDefaultBranch\")),\n pixelThreshold: asString(form.get(\"pixelThreshold\")),\n maxDiffRatio: asString(form.get(\"maxDiffRatio\")),\n publicBranchRegex: asString(form.get(\"publicBranchRegex\")),\n browser: asString(form.get(\"browser\")),\n };\n}\n\nasync function handleGeneralUpdate(c: Context): Promise<Response> {\n const slug = c.req.param(\"slug\") ?? \"\";\n const project = await findProject(slug);\n const form = await c.req.formData();\n const name = form.get(\"name\");\n if (typeof name !== \"string\" || name.trim() === \"\") {\n return c.html(\n (await renderSettingsPage(c, \"general\", { errors: { name: \"Name is required\" } })) ?? \"\",\n 400,\n );\n }\n const fields: GeneralFields = { ...readGeneralFields(form), name: name.trim() };\n return await persistGeneralUpdate(c, project, slug, fields);\n}\n\n/** Refuse, with 400 and the reason, a browser the runner cannot render (ADR 0007, decision 7). */\nasync function assertBrowserChoice(project: Project, browser: string | undefined): Promise<void> {\n if (browser !== undefined) {\n await assertTargetsSelectable([browser], resolveBrowsers(project));\n }\n}\n\nasync function persistGeneralUpdate(\n c: Context,\n project: Project,\n slug: string,\n fields: GeneralFields,\n): Promise<Response> {\n try {\n await assertBrowserChoice(project, fields.browser);\n await new ProjectModel(getStore().db).update(project.id, {\n name: fields.name,\n gitRepository: fields.gitRepository ?? undefined,\n gitDefaultBranch: fields.gitDefaultBranch ?? undefined,\n pixelThreshold: fields.pixelThreshold ? Number(fields.pixelThreshold) : undefined,\n maxDiffRatio: fields.maxDiffRatio ? Number(fields.maxDiffRatio) : undefined,\n publicBranchRegex:\n fields.publicBranchRegex === \"\" ? null : (fields.publicBranchRegex ?? undefined),\n browser: fields.browser ?? undefined,\n // Picking a different single engine returns the project to one-browser capture.\n ...(fields.browser && fields.browser !== project.browser ? { browsers: null } : {}),\n });\n flash(c, \"Project settings saved\");\n return hxRedirect(c, `/projects/${slug}/settings`);\n } catch (error) {\n const message = error instanceof Error ? error.message : \"Failed to update\";\n return c.html((await renderSettingsPage(c, \"general\", { globalError: message })) ?? \"\", 400);\n }\n}\n\nasync function handleTestsUpdate(c: Context): Promise<Response> {\n const slug = c.req.param(\"slug\") ?? \"\";\n const project = await findProject(slug);\n const form = await c.req.formData();\n const { executePlay, runA11y, runTests, playTimeoutMs } = readTestsFields(form);\n const rangeError = playTimeoutError(playTimeoutMs);\n if (rangeError) {\n return c.html((await renderSettingsPage(c, \"tests\", { globalError: rangeError })) ?? \"\", 400);\n }\n return await persistTestsUpdate(\n c,\n project,\n slug,\n { executePlay, runA11y, runTests },\n playTimeoutMs,\n );\n}\n\n/** Read the tests-tab form fields. */\nfunction readTestsFields(form: FormData): {\n executePlay: boolean;\n runA11y: boolean;\n runTests: boolean;\n playTimeoutMs: number | undefined;\n} {\n const playTimeoutMsRaw = asString(form.get(\"playTimeoutMs\"));\n return {\n executePlay: form.get(\"executePlay\") === \"true\",\n runA11y: form.get(\"runA11y\") === \"true\",\n runTests: form.get(\"runTests\") === \"true\",\n playTimeoutMs: playTimeoutMsRaw ? Number(playTimeoutMsRaw) : undefined,\n };\n}\n\n/** Range error for the play timeout, or null when valid. */\nfunction playTimeoutError(playTimeoutMs: number | undefined): string | null {\n if (\n playTimeoutMs !== undefined &&\n (Number.isNaN(playTimeoutMs) || playTimeoutMs < 1000 || playTimeoutMs > 30_000)\n ) {\n return \"Play timeout must be between 1000 and 30000\";\n }\n return null;\n}\n\nasync function persistTestsUpdate(\n c: Context,\n project: Project,\n slug: string,\n switches: { executePlay: boolean; runA11y: boolean; runTests: boolean },\n playTimeoutMs: number | undefined,\n): Promise<Response> {\n try {\n await new ProjectModel(getStore().db).update(project.id, {\n ...switches,\n playTimeoutMs: playTimeoutMs ?? undefined,\n });\n flash(c, \"Test settings saved\");\n return hxRedirect(c, `/projects/${slug}/settings/tests`);\n } catch (error) {\n const message = error instanceof Error ? error.message : \"Failed to update\";\n return c.html((await renderSettingsPage(c, \"tests\", { globalError: message })) ?? \"\", 400);\n }\n}\n\nasync function handleDeleteProject(c: Context): Promise<Response> {\n const project = await findProject(c.req.param(\"slug\") ?? \"\");\n await new ProjectModel(getStore().db).remove(project.id);\n flash(c, `Deleted ${project.name}`);\n return hxRedirect(c, \"/projects\");\n}\n\n/** Largest approval count the form and the API accept. */\nconst MAX_APPROVALS = 20;\n\n/**\n * Review settings (ADR 0013, decision 1): the switch and the approvals a build needs. Project\n * admins only. The route is classed `hosting`, not `review`: a review route answers 404 while\n * review is off, and this is the form that turns it back on. Turning review off accepts the\n * project's pending builds that no capture worked on, in one pass.\n */\nasync function handleReviewUpdate(c: Context): Promise<Response> {\n const slug = c.req.param(\"slug\") ?? \"\";\n const project = await resolveAuthorizedProject(c, slug, \"admin\");\n const form = await c.req.formData();\n const approvals = approvalsOf(form, project);\n if (approvals === null) {\n return c.html(\n (await renderSettingsPage(c, \"general\", {\n errors: { reviewApprovals: `Enter a whole number from 1 to ${MAX_APPROVALS}` },\n })) ?? \"\",\n 400,\n );\n }\n const updated = await new ProjectModel(getStore().db).update(project.id, {\n reviewEnabled: form.get(\"reviewEnabled\") === \"true\",\n reviewApprovals: approvals,\n });\n const accepted = await runSwitchOffPass(project, updated);\n flash(c, reviewSavedMessage(accepted));\n return hxRedirect(c, `/projects/${slug}/settings`);\n}\n\n/** The approvals count of the form (the current one when blank); null when it is not 1 to 20. */\nfunction approvalsOf(form: FormData, project: Project): number | null {\n const raw = asString(form.get(\"reviewApprovals\"));\n const approvals = raw ? Number(raw) : (project.reviewApprovals ?? 1);\n return Number.isInteger(approvals) && approvals >= 1 && approvals <= MAX_APPROVALS\n ? approvals\n : null;\n}\n\nfunction reviewSavedMessage(accepted: number): string {\n return accepted > 0\n ? `Review settings saved; accepted ${accepted} pending build${accepted === 1 ? \"\" : \"s\"}`\n : \"Review settings saved\";\n}\n"],"mappings":"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"}