@storyshelf/app 0.6.2 → 0.8.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 (429) hide show
  1. package/README.md +42 -4
  2. package/dist/app-types.d.mts +2 -6
  3. package/dist/asset-manifest.mjs +14 -7
  4. package/dist/asset-manifest.mjs.map +1 -1
  5. package/dist/assets/{htmx.min.js → htmx.mjs} +9 -1
  6. package/dist/assets/htmx.mjs.map +1 -0
  7. package/dist/assets/mark.mjs +9 -0
  8. package/dist/assets/mark.mjs.map +1 -0
  9. package/dist/boot-tasks.mjs +79 -0
  10. package/dist/boot-tasks.mjs.map +1 -0
  11. package/dist/build-state-flags.mjs +17 -0
  12. package/dist/build-state-flags.mjs.map +1 -0
  13. package/dist/build-state-setup.mjs +74 -0
  14. package/dist/build-state-setup.mjs.map +1 -0
  15. package/dist/capture-alerts.mjs +45 -0
  16. package/dist/capture-alerts.mjs.map +1 -0
  17. package/dist/capture-process.mjs +58 -0
  18. package/dist/capture-process.mjs.map +1 -0
  19. package/dist/capture-setup.mjs +96 -119
  20. package/dist/capture-setup.mjs.map +1 -1
  21. package/dist/connections.mjs +33 -0
  22. package/dist/connections.mjs.map +1 -0
  23. package/dist/consumer-setup.mjs +57 -0
  24. package/dist/consumer-setup.mjs.map +1 -0
  25. package/dist/credential-keys.mjs +1 -2
  26. package/dist/credentials.mjs +1 -2
  27. package/dist/git-status.mjs +45 -0
  28. package/dist/git-status.mjs.map +1 -0
  29. package/dist/hidden-data.mjs +23 -0
  30. package/dist/hidden-data.mjs.map +1 -0
  31. package/dist/index.d.mts +3 -5
  32. package/dist/index.mjs +204 -127
  33. package/dist/index.mjs.map +1 -1
  34. package/dist/insights/budget.mjs +48 -0
  35. package/dist/insights/budget.mjs.map +1 -0
  36. package/dist/insights/deps.mjs +20 -0
  37. package/dist/insights/deps.mjs.map +1 -0
  38. package/dist/insights/errors.mjs +21 -0
  39. package/dist/insights/errors.mjs.map +1 -0
  40. package/dist/insights/health-panel.mjs +39 -0
  41. package/dist/insights/health-panel.mjs.map +1 -0
  42. package/dist/insights/job.mjs +102 -0
  43. package/dist/insights/job.mjs.map +1 -0
  44. package/dist/insights/load-health.mjs +58 -0
  45. package/dist/insights/load-health.mjs.map +1 -0
  46. package/dist/insights/load-triage.mjs +97 -0
  47. package/dist/insights/load-triage.mjs.map +1 -0
  48. package/dist/insights/panel.mjs +27 -0
  49. package/dist/insights/panel.mjs.map +1 -0
  50. package/dist/insights/request.mjs +112 -0
  51. package/dist/insights/request.mjs.map +1 -0
  52. package/dist/insights/stale.mjs +20 -0
  53. package/dist/insights/stale.mjs.map +1 -0
  54. package/dist/insights/usage-report.mjs +59 -0
  55. package/dist/insights/usage-report.mjs.map +1 -0
  56. package/dist/insights/view.mjs +47 -0
  57. package/dist/insights/view.mjs.map +1 -0
  58. package/dist/job-canceller.mjs +13 -0
  59. package/dist/job-canceller.mjs.map +1 -0
  60. package/dist/job-queue.mjs +50 -0
  61. package/dist/job-queue.mjs.map +1 -0
  62. package/dist/jobs-setup.mjs +131 -0
  63. package/dist/jobs-setup.mjs.map +1 -0
  64. package/dist/lifecycle.mjs +93 -22
  65. package/dist/lifecycle.mjs.map +1 -1
  66. package/dist/middleware/auth-gate.mjs +5 -4
  67. package/dist/middleware/auth-gate.mjs.map +1 -1
  68. package/dist/middleware/csrf.mjs +1 -2
  69. package/dist/middleware/rate-limit.mjs +42 -35
  70. package/dist/middleware/rate-limit.mjs.map +1 -1
  71. package/dist/middleware/request-log.mjs +4 -4
  72. package/dist/middleware/request-log.mjs.map +1 -1
  73. package/dist/middleware/require-review.mjs +50 -0
  74. package/dist/middleware/require-review.mjs.map +1 -0
  75. package/dist/middleware/require-testing.mjs +40 -0
  76. package/dist/middleware/require-testing.mjs.map +1 -0
  77. package/dist/middleware/server-timing.mjs +1 -2
  78. package/dist/middleware/setup-gate.mjs +1 -2
  79. package/dist/middleware/setup-gate.mjs.map +1 -1
  80. package/dist/middleware/store-scope.mjs +13 -4
  81. package/dist/middleware/store-scope.mjs.map +1 -1
  82. package/dist/notifications-pause.mjs +32 -0
  83. package/dist/notifications-pause.mjs.map +1 -0
  84. package/dist/notify.d.mts +3 -4
  85. package/dist/notify.mjs +79 -41
  86. package/dist/notify.mjs.map +1 -1
  87. package/dist/pages/a11y-violations.mjs +115 -0
  88. package/dist/pages/a11y-violations.mjs.map +1 -0
  89. package/dist/pages/admin-ai.mjs +77 -0
  90. package/dist/pages/admin-ai.mjs.map +1 -0
  91. package/dist/pages/admin-system-origin.mjs +132 -0
  92. package/dist/pages/admin-system-origin.mjs.map +1 -0
  93. package/dist/pages/admin-system-targets.mjs +52 -0
  94. package/dist/pages/admin-system-targets.mjs.map +1 -0
  95. package/dist/pages/admin-system.mjs +64 -15
  96. package/dist/pages/admin-system.mjs.map +1 -1
  97. package/dist/pages/build-detail.mjs +105 -78
  98. package/dist/pages/build-detail.mjs.map +1 -1
  99. package/dist/pages/build-diff-actions.mjs +88 -35
  100. package/dist/pages/build-diff-actions.mjs.map +1 -1
  101. package/dist/pages/build-diff-comments.mjs +17 -16
  102. package/dist/pages/build-diff-comments.mjs.map +1 -1
  103. package/dist/pages/build-diff-default.mjs +53 -0
  104. package/dist/pages/build-diff-default.mjs.map +1 -0
  105. package/dist/pages/build-diff-environment.mjs +81 -0
  106. package/dist/pages/build-diff-environment.mjs.map +1 -0
  107. package/dist/pages/build-diff-header.mjs +23 -6
  108. package/dist/pages/build-diff-header.mjs.map +1 -1
  109. package/dist/pages/build-diff-insight.mjs +112 -0
  110. package/dist/pages/build-diff-insight.mjs.map +1 -0
  111. package/dist/pages/build-diff-model.mjs +74 -12
  112. package/dist/pages/build-diff-model.mjs.map +1 -1
  113. package/dist/pages/build-diff-nav.mjs +29 -14
  114. package/dist/pages/build-diff-nav.mjs.map +1 -1
  115. package/dist/pages/build-diff-reference.mjs +84 -0
  116. package/dist/pages/build-diff-reference.mjs.map +1 -0
  117. package/dist/pages/build-diff-shortcuts.mjs +6 -3
  118. package/dist/pages/build-diff-shortcuts.mjs.map +1 -1
  119. package/dist/pages/build-diff-stale.mjs +1 -2
  120. package/dist/pages/build-diff-usage.mjs +62 -0
  121. package/dist/pages/build-diff-usage.mjs.map +1 -0
  122. package/dist/pages/build-diff-viewer.mjs +97 -14
  123. package/dist/pages/build-diff-viewer.mjs.map +1 -1
  124. package/dist/pages/build-diff.mjs +37 -22
  125. package/dist/pages/build-diff.mjs.map +1 -1
  126. package/dist/pages/build-tests.mjs +73 -0
  127. package/dist/pages/build-tests.mjs.map +1 -0
  128. package/dist/pages/component-detail.mjs +248 -0
  129. package/dist/pages/component-detail.mjs.map +1 -0
  130. package/dist/pages/component-stories.mjs +167 -0
  131. package/dist/pages/component-stories.mjs.map +1 -0
  132. package/dist/pages/compute-jobs.mjs +14 -28
  133. package/dist/pages/compute-jobs.mjs.map +1 -1
  134. package/dist/pages/design-links.mjs +181 -0
  135. package/dist/pages/design-links.mjs.map +1 -0
  136. package/dist/pages/environment-change.mjs +52 -0
  137. package/dist/pages/environment-change.mjs.map +1 -0
  138. package/dist/pages/health-panel.mjs +125 -0
  139. package/dist/pages/health-panel.mjs.map +1 -0
  140. package/dist/pages/invite.mjs +1 -2
  141. package/dist/pages/label-detail.mjs +1 -2
  142. package/dist/pages/library-grid.mjs +104 -0
  143. package/dist/pages/library-grid.mjs.map +1 -0
  144. package/dist/pages/library-model.mjs +46 -0
  145. package/dist/pages/library-model.mjs.map +1 -0
  146. package/dist/pages/library-snapshots.mjs +129 -0
  147. package/dist/pages/library-snapshots.mjs.map +1 -0
  148. package/dist/pages/library.mjs +113 -220
  149. package/dist/pages/library.mjs.map +1 -1
  150. package/dist/pages/login.mjs +1 -2
  151. package/dist/pages/notifications-paused.mjs +21 -0
  152. package/dist/pages/notifications-paused.mjs.map +1 -0
  153. package/dist/pages/passkey-ceremony.mjs +1 -2
  154. package/dist/pages/profile.mjs +54 -50
  155. package/dist/pages/profile.mjs.map +1 -1
  156. package/dist/pages/project-builds-table.mjs +3 -4
  157. package/dist/pages/project-builds-table.mjs.map +1 -1
  158. package/dist/pages/project-builds.mjs +52 -23
  159. package/dist/pages/project-builds.mjs.map +1 -1
  160. package/dist/pages/project-create.mjs +1 -2
  161. package/dist/pages/project-settings.mjs +52 -14
  162. package/dist/pages/project-settings.mjs.map +1 -1
  163. package/dist/pages/projects-card.mjs +15 -3
  164. package/dist/pages/projects-card.mjs.map +1 -1
  165. package/dist/pages/projects.mjs +7 -4
  166. package/dist/pages/projects.mjs.map +1 -1
  167. package/dist/pages/review-changeset.mjs +115 -0
  168. package/dist/pages/review-changeset.mjs.map +1 -0
  169. package/dist/pages/review-comments.mjs +102 -0
  170. package/dist/pages/review-comments.mjs.map +1 -0
  171. package/dist/pages/review-verdict.mjs +101 -0
  172. package/dist/pages/review-verdict.mjs.map +1 -0
  173. package/dist/pages/root.mjs +1 -2
  174. package/dist/pages/settings-ai.mjs +68 -0
  175. package/dist/pages/settings-ai.mjs.map +1 -0
  176. package/dist/pages/settings-capture.mjs +123 -0
  177. package/dist/pages/settings-capture.mjs.map +1 -0
  178. package/dist/pages/settings-design-credential.mjs +98 -0
  179. package/dist/pages/settings-design-credential.mjs.map +1 -0
  180. package/dist/pages/settings-design-links.mjs +178 -0
  181. package/dist/pages/settings-design-links.mjs.map +1 -0
  182. package/dist/pages/settings-design.mjs +47 -0
  183. package/dist/pages/settings-design.mjs.map +1 -0
  184. package/dist/pages/settings-general.mjs +47 -23
  185. package/dist/pages/settings-general.mjs.map +1 -1
  186. package/dist/pages/settings-labels.mjs +44 -4
  187. package/dist/pages/settings-labels.mjs.map +1 -1
  188. package/dist/pages/settings-links.mjs +111 -0
  189. package/dist/pages/settings-links.mjs.map +1 -0
  190. package/dist/pages/settings-members.mjs +1 -2
  191. package/dist/pages/settings-notifications-git.mjs +127 -0
  192. package/dist/pages/settings-notifications-git.mjs.map +1 -0
  193. package/dist/pages/settings-notifications.mjs +80 -44
  194. package/dist/pages/settings-notifications.mjs.map +1 -1
  195. package/dist/pages/settings-tests.mjs +34 -4
  196. package/dist/pages/settings-tests.mjs.map +1 -1
  197. package/dist/pages/settings-tokens.mjs +1 -2
  198. package/dist/pages/settings-webhooks.mjs +1 -2
  199. package/dist/pages/storybook.mjs +1 -2
  200. package/dist/pages/target-choices.mjs +38 -0
  201. package/dist/pages/target-choices.mjs.map +1 -0
  202. package/dist/publish-setup.mjs +88 -0
  203. package/dist/publish-setup.mjs.map +1 -0
  204. package/dist/retention-timer.mjs +12 -12
  205. package/dist/retention-timer.mjs.map +1 -1
  206. package/dist/review-events.mjs +47 -0
  207. package/dist/review-events.mjs.map +1 -0
  208. package/dist/routers/admin-pages.mjs +46 -5
  209. package/dist/routers/admin-pages.mjs.map +1 -1
  210. package/dist/routers/admin-public-origin.mjs +92 -0
  211. package/dist/routers/admin-public-origin.mjs.map +1 -0
  212. package/dist/routers/admin.mjs +8 -4
  213. package/dist/routers/admin.mjs.map +1 -1
  214. package/dist/routers/assets.mjs +10 -4
  215. package/dist/routers/assets.mjs.map +1 -1
  216. package/dist/routers/attempts.mjs +1 -2
  217. package/dist/routers/auth-sync.mjs +1 -2
  218. package/dist/routers/auth.mjs +2 -3
  219. package/dist/routers/baseline-guard.mjs +4 -5
  220. package/dist/routers/baseline-guard.mjs.map +1 -1
  221. package/dist/routers/baseline-reference.mjs +72 -0
  222. package/dist/routers/baseline-reference.mjs.map +1 -0
  223. package/dist/routers/baselines.handlers.mjs +63 -0
  224. package/dist/routers/baselines.handlers.mjs.map +1 -0
  225. package/dist/routers/baselines.mjs +124 -0
  226. package/dist/routers/baselines.mjs.map +1 -0
  227. package/dist/routers/build-cancel.mjs +67 -0
  228. package/dist/routers/build-cancel.mjs.map +1 -0
  229. package/dist/routers/build-changeset.mjs +69 -0
  230. package/dist/routers/build-changeset.mjs.map +1 -0
  231. package/dist/routers/build-reviews.handlers.mjs +85 -0
  232. package/dist/routers/build-reviews.handlers.mjs.map +1 -0
  233. package/dist/routers/build-reviews.mjs +73 -0
  234. package/dist/routers/build-reviews.mjs.map +1 -0
  235. package/dist/routers/builds.handlers.mjs +65 -60
  236. package/dist/routers/builds.handlers.mjs.map +1 -1
  237. package/dist/routers/builds.mjs +77 -19
  238. package/dist/routers/builds.mjs.map +1 -1
  239. package/dist/routers/capture-cache.mjs +29 -0
  240. package/dist/routers/capture-cache.mjs.map +1 -0
  241. package/dist/routers/capture-config.mjs +34 -0
  242. package/dist/routers/capture-config.mjs.map +1 -0
  243. package/dist/routers/comments.mjs +42 -17
  244. package/dist/routers/comments.mjs.map +1 -1
  245. package/dist/routers/comparison-image.mjs +29 -0
  246. package/dist/routers/comparison-image.mjs.map +1 -0
  247. package/dist/routers/component-catalog.handlers.mjs +70 -0
  248. package/dist/routers/component-catalog.handlers.mjs.map +1 -0
  249. package/dist/routers/component.handlers.mjs +120 -0
  250. package/dist/routers/component.handlers.mjs.map +1 -0
  251. package/dist/routers/connections.mjs +117 -0
  252. package/dist/routers/connections.mjs.map +1 -0
  253. package/dist/routers/design-connect.handlers.mjs +43 -0
  254. package/dist/routers/design-connect.handlers.mjs.map +1 -0
  255. package/dist/routers/design.handlers.mjs +113 -0
  256. package/dist/routers/design.handlers.mjs.map +1 -0
  257. package/dist/routers/flash.mjs +1 -2
  258. package/dist/routers/health-report.mjs +3 -5
  259. package/dist/routers/health-report.mjs.map +1 -1
  260. package/dist/routers/health.mjs +5 -5
  261. package/dist/routers/health.mjs.map +1 -1
  262. package/dist/routers/helpers.mjs +20 -5
  263. package/dist/routers/helpers.mjs.map +1 -1
  264. package/dist/routers/htmx.mjs +1 -2
  265. package/dist/routers/insights-ui.mjs +63 -0
  266. package/dist/routers/insights-ui.mjs.map +1 -0
  267. package/dist/routers/insights.handlers.mjs +44 -0
  268. package/dist/routers/insights.handlers.mjs.map +1 -0
  269. package/dist/routers/insights.mjs +188 -0
  270. package/dist/routers/insights.mjs.map +1 -0
  271. package/dist/routers/labels.mjs +1 -2
  272. package/dist/routers/links.handlers.mjs +79 -0
  273. package/dist/routers/links.handlers.mjs.map +1 -0
  274. package/dist/routers/links.mjs +138 -0
  275. package/dist/routers/links.mjs.map +1 -0
  276. package/dist/routers/media.mjs +30 -7
  277. package/dist/routers/media.mjs.map +1 -1
  278. package/dist/routers/members.mjs +3 -3
  279. package/dist/routers/members.mjs.map +1 -1
  280. package/dist/routers/notification-channels.mjs +18 -25
  281. package/dist/routers/notification-channels.mjs.map +1 -1
  282. package/dist/routers/notification-subscriptions.mjs +1 -2
  283. package/dist/routers/oauth-state.mjs +44 -0
  284. package/dist/routers/oauth-state.mjs.map +1 -0
  285. package/dist/routers/preview-base.mjs +37 -0
  286. package/dist/routers/preview-base.mjs.map +1 -0
  287. package/dist/routers/preview-token.mjs +50 -0
  288. package/dist/routers/preview-token.mjs.map +1 -0
  289. package/dist/routers/profile.mjs +4 -3
  290. package/dist/routers/profile.mjs.map +1 -1
  291. package/dist/routers/projects.mjs +18 -3
  292. package/dist/routers/projects.mjs.map +1 -1
  293. package/dist/routers/publish-upload.mjs +85 -0
  294. package/dist/routers/publish-upload.mjs.map +1 -0
  295. package/dist/routers/published-access.mjs +51 -0
  296. package/dist/routers/published-access.mjs.map +1 -0
  297. package/dist/routers/published-compression.mjs +109 -0
  298. package/dist/routers/published-compression.mjs.map +1 -0
  299. package/dist/routers/published-files.mjs +69 -0
  300. package/dist/routers/published-files.mjs.map +1 -0
  301. package/dist/routers/review-forms.mjs +71 -0
  302. package/dist/routers/review-forms.mjs.map +1 -0
  303. package/dist/routers/review-next.mjs +1 -2
  304. package/dist/routers/review-section.handlers.mjs +84 -0
  305. package/dist/routers/review-section.handlers.mjs.map +1 -0
  306. package/dist/routers/review-switch-off.mjs +28 -0
  307. package/dist/routers/review-switch-off.mjs.map +1 -0
  308. package/dist/routers/schemas.mjs +141 -24
  309. package/dist/routers/schemas.mjs.map +1 -1
  310. package/dist/routers/scoped-models.mjs +21 -0
  311. package/dist/routers/scoped-models.mjs.map +1 -0
  312. package/dist/routers/settings-ai.mjs +34 -0
  313. package/dist/routers/settings-ai.mjs.map +1 -0
  314. package/dist/routers/settings-capture.mjs +76 -0
  315. package/dist/routers/settings-capture.mjs.map +1 -0
  316. package/dist/routers/settings-design.mjs +102 -0
  317. package/dist/routers/settings-design.mjs.map +1 -0
  318. package/dist/routers/settings-general.mjs +56 -12
  319. package/dist/routers/settings-general.mjs.map +1 -1
  320. package/dist/routers/settings-git-channel.mjs +76 -0
  321. package/dist/routers/settings-git-channel.mjs.map +1 -0
  322. package/dist/routers/settings-labels.mjs +28 -3
  323. package/dist/routers/settings-labels.mjs.map +1 -1
  324. package/dist/routers/settings-links.mjs +62 -0
  325. package/dist/routers/settings-links.mjs.map +1 -0
  326. package/dist/routers/settings-members.mjs +5 -9
  327. package/dist/routers/settings-members.mjs.map +1 -1
  328. package/dist/routers/settings-notifications.mjs +16 -12
  329. package/dist/routers/settings-notifications.mjs.map +1 -1
  330. package/dist/routers/settings-tokens.mjs +3 -7
  331. package/dist/routers/settings-tokens.mjs.map +1 -1
  332. package/dist/routers/settings-webhooks.mjs +8 -26
  333. package/dist/routers/settings-webhooks.mjs.map +1 -1
  334. package/dist/routers/settings.handlers.mjs +108 -28
  335. package/dist/routers/settings.handlers.mjs.map +1 -1
  336. package/dist/routers/settings.mjs +17 -6
  337. package/dist/routers/settings.mjs.map +1 -1
  338. package/dist/routers/signed-storybook.mjs +55 -0
  339. package/dist/routers/signed-storybook.mjs.map +1 -0
  340. package/dist/routers/snapshot-groups.mjs +62 -0
  341. package/dist/routers/snapshot-groups.mjs.map +1 -0
  342. package/dist/routers/snapshots.mjs +104 -7
  343. package/dist/routers/snapshots.mjs.map +1 -1
  344. package/dist/routers/storybook.mjs +57 -104
  345. package/dist/routers/storybook.mjs.map +1 -1
  346. package/dist/routers/tokens.mjs +3 -7
  347. package/dist/routers/tokens.mjs.map +1 -1
  348. package/dist/routers/ui.mjs +38 -7
  349. package/dist/routers/ui.mjs.map +1 -1
  350. package/dist/routers/webhooks.mjs +6 -13
  351. package/dist/routers/webhooks.mjs.map +1 -1
  352. package/dist/routers/well-known.mjs +12 -6
  353. package/dist/routers/well-known.mjs.map +1 -1
  354. package/dist/routes/api-routes.mjs +92 -0
  355. package/dist/routes/api-routes.mjs.map +1 -0
  356. package/dist/routes/classification.mjs +37 -0
  357. package/dist/routes/classification.mjs.map +1 -0
  358. package/dist/routes/openapi-classes.mjs +21 -0
  359. package/dist/routes/openapi-classes.mjs.map +1 -0
  360. package/dist/routes/page-routes.mjs +103 -0
  361. package/dist/routes/page-routes.mjs.map +1 -0
  362. package/dist/runtime.mjs +19 -15
  363. package/dist/runtime.mjs.map +1 -1
  364. package/dist/serverless.mjs +4 -0
  365. package/dist/store.mjs +20 -3
  366. package/dist/store.mjs.map +1 -1
  367. package/dist/targets.mjs +45 -0
  368. package/dist/targets.mjs.map +1 -0
  369. package/dist/ui/avatar.mjs +1 -2
  370. package/dist/ui/buttons.mjs +17 -5
  371. package/dist/ui/buttons.mjs.map +1 -1
  372. package/dist/ui/code-block.mjs +1 -2
  373. package/dist/ui/compare-stage.mjs +86 -54
  374. package/dist/ui/compare-stage.mjs.map +1 -1
  375. package/dist/ui/csrf-field.mjs +1 -2
  376. package/dist/ui/css.mjs +1 -2
  377. package/dist/ui/design-reference.mjs +96 -0
  378. package/dist/ui/design-reference.mjs.map +1 -0
  379. package/dist/ui/document.mjs +2 -4
  380. package/dist/ui/document.mjs.map +1 -1
  381. package/dist/ui/dropdown.mjs +1 -2
  382. package/dist/ui/feedback.mjs +1 -2
  383. package/dist/ui/filter-input.mjs +1 -2
  384. package/dist/ui/forms.mjs +7 -7
  385. package/dist/ui/forms.mjs.map +1 -1
  386. package/dist/ui/icons/icon.mjs +1 -2
  387. package/dist/ui/icons/paths.mjs +2 -2
  388. package/dist/ui/icons/paths.mjs.map +1 -1
  389. package/dist/ui/icons/sprite.mjs +1 -2
  390. package/dist/ui/kbd.mjs +1 -2
  391. package/dist/ui/layout.mjs +1 -2
  392. package/dist/ui/preview-embed.mjs +81 -0
  393. package/dist/ui/preview-embed.mjs.map +1 -0
  394. package/dist/ui/progress.mjs +1 -2
  395. package/dist/ui/segmented.mjs +1 -2
  396. package/dist/ui/shell/account.mjs +1 -2
  397. package/dist/ui/shell/bare.mjs +1 -2
  398. package/dist/ui/shell/boot-script.mjs +1 -2
  399. package/dist/ui/shell/client-script.mjs +86 -2
  400. package/dist/ui/shell/client-script.mjs.map +1 -1
  401. package/dist/ui/shell/nav.mjs +13 -10
  402. package/dist/ui/shell/nav.mjs.map +1 -1
  403. package/dist/ui/shell/review-script.mjs +25 -3
  404. package/dist/ui/shell/review-script.mjs.map +1 -1
  405. package/dist/ui/shell/sidebar.mjs +3 -4
  406. package/dist/ui/shell/sidebar.mjs.map +1 -1
  407. package/dist/ui/shell/topbar.mjs +1 -2
  408. package/dist/ui/stacks.mjs +1 -2
  409. package/dist/ui/styles/base.mjs +1 -2
  410. package/dist/ui/styles/behavior.mjs +1 -2
  411. package/dist/ui/styles/review-layout.mjs +1 -2
  412. package/dist/ui/styles/review.mjs +1 -2
  413. package/dist/ui/styles/tokens.mjs +1 -2
  414. package/dist/ui/styles.mjs +1 -2
  415. package/dist/ui/subnav.mjs +1 -2
  416. package/dist/ui/table.mjs +1 -2
  417. package/dist/ui/theme.mjs +1 -2
  418. package/dist/ui/thumbnail.mjs +1 -2
  419. package/dist/ui/time.mjs +1 -2
  420. package/package.json +14 -12
  421. package/dist/asset-paths.mjs +0 -23
  422. package/dist/asset-paths.mjs.map +0 -1
  423. package/dist/assets/mark.svg +0 -1
  424. package/dist/pages/settings-status.mjs +0 -137
  425. package/dist/pages/settings-status.mjs.map +0 -1
  426. package/dist/routers/settings-status.mjs +0 -77
  427. package/dist/routers/settings-status.mjs.map +0 -1
  428. package/dist/routers/status-configs.mjs +0 -108
  429. package/dist/routers/status-configs.mjs.map +0 -1
@@ -2,8 +2,8 @@ import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
4
  import { ProjectModel } from "@storyshelf/core/models";
5
- import { Retention } from "@storyshelf/core/retention";
6
- //#region src/retention-timer.ts
5
+ import { Retention, purgeAiData } from "@storyshelf/core/retention";
6
+ // src/retention-timer.ts
7
7
  const DEFAULT_BRANCH_TTL_DAYS = 30;
8
8
  const DEFAULT_BRANCH_GC_INTERVAL_MS = 864e5;
9
9
  /**
@@ -17,16 +17,20 @@ const DEFAULT_BRANCH_GC_INTERVAL_MS = 864e5;
17
17
  * @param logger - Structured logger for sweep diagnostics
18
18
  * @returns Timer handle with `stop()`, or `null` if GC is disabled
19
19
  */
20
- function startBranchGcTimer(db, storage, config, logger) {
20
+ function startBranchGcTimer(db, storage, config, logger, testing = false) {
21
21
  const ttlDays = config.branchTtlDays ?? DEFAULT_BRANCH_TTL_DAYS;
22
22
  if (ttlDays === null) return null;
23
23
  const intervalMs = config.branchGcIntervalMs ?? DEFAULT_BRANCH_GC_INTERVAL_MS;
24
24
  if (intervalMs <= 0) return null;
25
- return scheduleBranchGc(createBranchGcRunner(db, storage, logger, ttlDays), intervalMs);
25
+ return scheduleBranchGc(createBranchGcRunner(db, storage, logger, ttlDays, testing), intervalMs);
26
26
  }
27
- function createBranchGcRunner(db, storage, logger, ttlDays) {
27
+ function createBranchGcRunner(db, storage, logger, ttlDays, testing) {
28
28
  return async () => {
29
29
  try {
30
+ if (testing) {
31
+ await purgeAiData(db, /* @__PURE__ */ new Date(), logger);
32
+ return;
33
+ }
30
34
  const projects = await new ProjectModel(db).list();
31
35
  const retention = new Retention(db, storage, {
32
36
  builds: db.tables.builds,
@@ -36,6 +40,7 @@ function createBranchGcRunner(db, storage, logger, ttlDays) {
36
40
  const results = await Promise.all(projects.map((project) => retention.purgeStaleBranches(project, ttlDays)));
37
41
  const totalBranches = results.reduce((sum, r) => sum + r.removedBranches, 0);
38
42
  const totalBaselines = results.reduce((sum, r) => sum + r.removedBaselines, 0);
43
+ await purgeAiData(db, /* @__PURE__ */ new Date(), logger);
39
44
  if (totalBranches > 0 || totalBaselines > 0) logger.info({
40
45
  totalBranches,
41
46
  totalBaselines
@@ -46,20 +51,15 @@ function createBranchGcRunner(db, storage, logger, ttlDays) {
46
51
  };
47
52
  }
48
53
  function scheduleBranchGc(run, intervalMs) {
49
- const timeout = setTimeout(() => {
50
- run();
51
- }, Math.min(36e5, intervalMs));
54
+ const timeout = setTimeout(() => run(), Math.min(36e5, intervalMs));
52
55
  timeout.unref?.();
53
- const interval = setInterval(() => {
54
- run();
55
- }, intervalMs);
56
+ const interval = setInterval(() => run(), intervalMs);
56
57
  interval.unref?.();
57
58
  return { stop: () => {
58
59
  clearTimeout(timeout);
59
60
  clearInterval(interval);
60
61
  } };
61
62
  }
62
- //#endregion
63
63
  export { startBranchGcTimer };
64
64
 
65
65
  //# sourceMappingURL=retention-timer.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"retention-timer.mjs","names":[],"sources":["../src/retention-timer.ts"],"sourcesContent":["import type { DatabaseAdapter } from \"@storyshelf/core/adapter/database\";\nimport type { StorageAdapter } from \"@storyshelf/core/adapter/storage\";\nimport type { ShelfConfig } from \"@storyshelf/core/config\";\nimport type { Logger } from \"@storyshelf/core/logger\";\n/* oxlint-disable typescript/promise-function-async -- interval helpers return promise factories */\nimport { ProjectModel } from \"@storyshelf/core/models\";\nimport { Retention } from \"@storyshelf/core/retention\";\n\nconst DEFAULT_BRANCH_TTL_DAYS = 30;\nconst DEFAULT_BRANCH_GC_INTERVAL_MS = 86_400_000;\n\n/** Handle for the background branch-GC interval; call `stop()` on server shutdown. */\nexport interface BranchGcTimer {\n stop(): void;\n}\n\n/**\n * Start a daily sweep that removes baselines for branches that have not\n * been updated within `branchTtlDays`. Returns a timer handle or `null`\n * when GC is disabled (`branchTtlDays: null` or non-positive interval).\n *\n * @param db - Database adapter to list projects\n * @param storage - Storage adapter for baseline cleanup\n * @param config - Shelf config supplying `branchTtlDays` and `branchGcIntervalMs`\n * @param logger - Structured logger for sweep diagnostics\n * @returns Timer handle with `stop()`, or `null` if GC is disabled\n */\nexport function startBranchGcTimer(\n db: DatabaseAdapter,\n storage: StorageAdapter,\n config: ShelfConfig,\n logger: Logger,\n): BranchGcTimer | null {\n const ttlDays = config.branchTtlDays ?? DEFAULT_BRANCH_TTL_DAYS;\n if (ttlDays === null) {\n return null;\n }\n const intervalMs = config.branchGcIntervalMs ?? DEFAULT_BRANCH_GC_INTERVAL_MS;\n if (intervalMs <= 0) {\n return null;\n }\n const run = createBranchGcRunner(db, storage, logger, ttlDays);\n return scheduleBranchGc(run, intervalMs);\n}\n\n// oxlint-disable-next-line typescript/promise-function-async\nfunction createBranchGcRunner(\n db: DatabaseAdapter,\n storage: StorageAdapter,\n logger: Logger,\n ttlDays: number,\n): () => Promise<void> {\n return async (): Promise<void> => {\n try {\n const projects = await new ProjectModel(db).list();\n const retention = new Retention(\n db,\n storage,\n {\n builds: db.tables.builds,\n buildLabels: db.tables.buildLabels,\n baselines: db.tables.baselines,\n },\n logger,\n );\n const results = await Promise.all(\n projects.map((project) => retention.purgeStaleBranches(project, ttlDays)),\n );\n const totalBranches = results.reduce(\n (sum: number, r: { removedBranches: number }) => sum + r.removedBranches,\n 0,\n );\n const totalBaselines = results.reduce(\n (sum: number, r: { removedBaselines: number }) => sum + r.removedBaselines,\n 0,\n );\n if (totalBranches > 0 || totalBaselines > 0) {\n logger.info({ totalBranches, totalBaselines }, \"branch GC daily sweep complete\");\n }\n } catch (error) {\n logger.error({ err: error }, \"branch GC sweep failed\");\n }\n };\n}\n\nfunction scheduleBranchGc(run: () => Promise<void>, intervalMs: number): BranchGcTimer {\n const firstDelay = Math.min(3_600_000, intervalMs);\n const timeout = setTimeout(() => {\n // oxlint-disable-next-line no-void -- fire-and-forget interval task\n void run();\n }, firstDelay);\n timeout.unref?.();\n\n const interval = setInterval(() => {\n // oxlint-disable-next-line no-void -- fire-and-forget interval task\n void run();\n }, intervalMs);\n interval.unref?.();\n\n return {\n stop: () => {\n clearTimeout(timeout);\n clearInterval(interval);\n },\n };\n}\n"],"mappings":";;;;;;AAQA,MAAM,0BAA0B;AAChC,MAAM,gCAAgC;;;;;;;;;;;;AAkBtC,SAAgB,mBACd,IACA,SACA,QACA,QACsB;CACtB,MAAM,UAAU,OAAO,iBAAiB;CACxC,IAAI,YAAY,MACd,OAAO;CAET,MAAM,aAAa,OAAO,sBAAsB;CAChD,IAAI,cAAc,GAChB,OAAO;CAGT,OAAO,iBADK,qBAAqB,IAAI,SAAS,QAAQ,OAC5B,GAAG,UAAU;AACzC;AAGA,SAAS,qBACP,IACA,SACA,QACA,SACqB;CACrB,OAAO,YAA2B;EAChC,IAAI;GACF,MAAM,WAAW,MAAM,IAAI,aAAa,EAAE,CAAC,CAAC,KAAK;GACjD,MAAM,YAAY,IAAI,UACpB,IACA,SACA;IACE,QAAQ,GAAG,OAAO;IAClB,aAAa,GAAG,OAAO;IACvB,WAAW,GAAG,OAAO;GACvB,GACA,MACF;GACA,MAAM,UAAU,MAAM,QAAQ,IAC5B,SAAS,KAAK,YAAY,UAAU,mBAAmB,SAAS,OAAO,CAAC,CAC1E;GACA,MAAM,gBAAgB,QAAQ,QAC3B,KAAa,MAAmC,MAAM,EAAE,iBACzD,CACF;GACA,MAAM,iBAAiB,QAAQ,QAC5B,KAAa,MAAoC,MAAM,EAAE,kBAC1D,CACF;GACA,IAAI,gBAAgB,KAAK,iBAAiB,GACxC,OAAO,KAAK;IAAE;IAAe;GAAe,GAAG,gCAAgC;EAEnF,SAAS,OAAO;GACd,OAAO,MAAM,EAAE,KAAK,MAAM,GAAG,wBAAwB;EACvD;CACF;AACF;AAEA,SAAS,iBAAiB,KAA0B,YAAmC;CAErF,MAAM,UAAU,iBAAiB;EAE/B,IAAS;CACX,GAJmB,KAAK,IAAI,MAAW,UAI3B,CAAC;CACb,QAAQ,QAAQ;CAEhB,MAAM,WAAW,kBAAkB;EAEjC,IAAS;CACX,GAAG,UAAU;CACb,SAAS,QAAQ;CAEjB,OAAO,EACL,YAAY;EACV,aAAa,OAAO;EACpB,cAAc,QAAQ;CACxB,EACF;AACF"}
1
+ {"version":3,"file":"retention-timer.mjs","names":[],"sources":["../src/retention-timer.ts"],"sourcesContent":["import type { DatabaseAdapter } from \"@storyshelf/core/adapter/database\";\nimport type { StorageAdapter } from \"@storyshelf/core/adapter/storage\";\nimport type { ShelfConfig } from \"@storyshelf/core/config\";\nimport type { Logger } from \"@storyshelf/core/logger\";\n/* oxlint-disable typescript/promise-function-async -- interval helpers return promise factories */\nimport { ProjectModel } from \"@storyshelf/core/models\";\nimport { Retention, purgeAiData } from \"@storyshelf/core/retention\";\n\nconst DEFAULT_BRANCH_TTL_DAYS = 30;\nconst DEFAULT_BRANCH_GC_INTERVAL_MS = 86_400_000;\n\n/** Handle for the background branch-GC interval; call `stop()` on server shutdown. */\nexport interface BranchGcTimer {\n stop(): void;\n}\n\n/**\n * Start a daily sweep that removes baselines for branches that have not\n * been updated within `branchTtlDays`. Returns a timer handle or `null`\n * when GC is disabled (`branchTtlDays: null` or non-positive interval).\n *\n * @param db - Database adapter to list projects\n * @param storage - Storage adapter for baseline cleanup\n * @param config - Shelf config supplying `branchTtlDays` and `branchGcIntervalMs`\n * @param logger - Structured logger for sweep diagnostics\n * @returns Timer handle with `stop()`, or `null` if GC is disabled\n */\nexport function startBranchGcTimer(\n db: DatabaseAdapter,\n storage: StorageAdapter,\n config: ShelfConfig,\n logger: Logger,\n testing = false,\n): BranchGcTimer | null {\n const ttlDays = config.branchTtlDays ?? DEFAULT_BRANCH_TTL_DAYS;\n if (ttlDays === null) {\n return null;\n }\n const intervalMs = config.branchGcIntervalMs ?? DEFAULT_BRANCH_GC_INTERVAL_MS;\n if (intervalMs <= 0) {\n return null;\n }\n const run = createBranchGcRunner(db, storage, logger, ttlDays, testing);\n return scheduleBranchGc(run, intervalMs);\n}\n\n// oxlint-disable-next-line typescript/promise-function-async\nfunction createBranchGcRunner(\n db: DatabaseAdapter,\n storage: StorageAdapter,\n logger: Logger,\n ttlDays: number,\n testing: boolean,\n): () => Promise<void> {\n // oxlint-disable-next-line eslint/max-statements -- the sweep and its error boundary are kept together\n return async (): Promise<void> => {\n try {\n // Baseline removal must be coordinated with references before testing deployments can GC.\n // Keep independent AI-data cleanup running even while branch GC is disabled.\n if (testing) {\n await purgeAiData(db, new Date(), logger);\n return;\n }\n const projects = await new ProjectModel(db).list();\n const retention = new Retention(\n db,\n storage,\n {\n builds: db.tables.builds,\n buildLabels: db.tables.buildLabels,\n baselines: db.tables.baselines,\n },\n logger,\n );\n const results = await Promise.all(\n projects.map((project) => retention.purgeStaleBranches(project, ttlDays)),\n );\n const totalBranches = results.reduce(\n (sum: number, r: { removedBranches: number }) => sum + r.removedBranches,\n 0,\n );\n const totalBaselines = results.reduce(\n (sum: number, r: { removedBaselines: number }) => sum + r.removedBaselines,\n 0,\n );\n await purgeAiData(db, new Date(), logger);\n if (totalBranches > 0 || totalBaselines > 0) {\n logger.info({ totalBranches, totalBaselines }, \"branch GC daily sweep complete\");\n }\n } catch (error) {\n logger.error({ err: error }, \"branch GC sweep failed\");\n }\n };\n}\n\nfunction scheduleBranchGc(run: () => Promise<void>, intervalMs: number): BranchGcTimer {\n const firstDelay = Math.min(3_600_000, intervalMs);\n // oxlint-disable-next-line typescript/no-misused-promises, typescript/strict-void-return -- run catches failures; returning it allows deterministic completion in tests\n const timeout = setTimeout(() => run(), firstDelay);\n timeout.unref?.();\n\n // oxlint-disable-next-line typescript/no-misused-promises, typescript/strict-void-return -- run catches failures\n const interval = setInterval(() => run(), intervalMs);\n interval.unref?.();\n\n return {\n stop: () => {\n clearTimeout(timeout);\n clearInterval(interval);\n },\n };\n}\n"],"mappings":";;;;;;AAQA,MAAM,0BAA0B;AAChC,MAAM,gCAAgC;;;;;;;;;;;;AAkBtC,SAAgB,mBACd,IACA,SACA,QACA,QACA,UAAU,OACY;CACtB,MAAM,UAAU,OAAO,iBAAiB;CACxC,IAAI,YAAY,MACd,OAAO;CAET,MAAM,aAAa,OAAO,sBAAsB;CAChD,IAAI,cAAc,GAChB,OAAO;CAGT,OAAO,iBADK,qBAAqB,IAAI,SAAS,QAAQ,SAAS,OACrC,GAAG,UAAU;AACzC;AAGA,SAAS,qBACP,IACA,SACA,QACA,SACA,SACqB;CAErB,OAAO,YAA2B;EAChC,IAAI;GAGF,IAAI,SAAS;IACX,MAAM,YAAY,oBAAI,IAAI,KAAK,GAAG,MAAM;IACxC;GACF;GACA,MAAM,WAAW,MAAM,IAAI,aAAa,EAAE,CAAC,CAAC,KAAK;GACjD,MAAM,YAAY,IAAI,UACpB,IACA,SACA;IACE,QAAQ,GAAG,OAAO;IAClB,aAAa,GAAG,OAAO;IACvB,WAAW,GAAG,OAAO;GACvB,GACA,MACF;GACA,MAAM,UAAU,MAAM,QAAQ,IAC5B,SAAS,KAAK,YAAY,UAAU,mBAAmB,SAAS,OAAO,CAAC,CAC1E;GACA,MAAM,gBAAgB,QAAQ,QAC3B,KAAa,MAAmC,MAAM,EAAE,iBACzD,CACF;GACA,MAAM,iBAAiB,QAAQ,QAC5B,KAAa,MAAoC,MAAM,EAAE,kBAC1D,CACF;GACA,MAAM,YAAY,oBAAI,IAAI,KAAK,GAAG,MAAM;GACxC,IAAI,gBAAgB,KAAK,iBAAiB,GACxC,OAAO,KAAK;IAAE;IAAe;GAAe,GAAG,gCAAgC;EAEnF,SAAS,OAAO;GACd,OAAO,MAAM,EAAE,KAAK,MAAM,GAAG,wBAAwB;EACvD;CACF;AACF;AAEA,SAAS,iBAAiB,KAA0B,YAAmC;CAGrF,MAAM,UAAU,iBAAiB,IAAI,GAFlB,KAAK,IAAI,MAAW,UAEU,CAAC;CAClD,QAAQ,QAAQ;CAGhB,MAAM,WAAW,kBAAkB,IAAI,GAAG,UAAU;CACpD,SAAS,QAAQ;CAEjB,OAAO,EACL,YAAY;EACV,aAAa,OAAO;EACpB,cAAc,QAAQ;CACxB,EACF;AACF"}
@@ -0,0 +1,47 @@
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 { notifyProjectWith } from "./notify.mjs";
7
+ import { emitWebhookEvent } from "@storyshelf/core/adapter/webhook-events";
8
+ // src/review-events.ts
9
+ /**
10
+ * Review events (ADR 0013, decisions 4 and 10; ADR 0014, decision 10). They ride the existing
11
+ * paths: signed webhooks to the project's subscribers and the notification fan-out (stored
12
+ * channels plus per-user subscriptions). There is no new transport.
13
+ *
14
+ * - `review:requested`: a build is waiting for a verdict. Default-on for the project's approvers
15
+ * and admins (see `notify.ts`).
16
+ * - `review:comment`: a build or story comment (snapshot comments keep `comment:created`).
17
+ * - `review:verdict`: a reviewer approved or rejected a build.
18
+ */
19
+ /** Event names of the review topics. */
20
+ const REVIEW_EVENTS = {
21
+ requested: "review:requested",
22
+ comment: "review:comment",
23
+ verdict: "review:verdict"
24
+ };
25
+ /** Webhook and notification fan-out for one review event, from explicit dependencies. */
26
+ async function announceReviewWith(deps, project, event, data, buildId) {
27
+ await emitWebhookEvent(deps.db, { webhooks: deps.db.tables.webhooks }, project.id, event, data, credentialKeys(deps.config), deps.logger);
28
+ await notifyProjectWith(deps, project, event, data, `/builds/${buildId}`);
29
+ }
30
+ /** {@link announceReviewWith} inside a request. */
31
+ async function announceReview(project, event, data, buildId) {
32
+ await announceReviewWith(getStore(), project, event, data, buildId);
33
+ }
34
+ /** What `review:requested` carries: the build to look at. */
35
+ function reviewRequestedData(build) {
36
+ return {
37
+ buildId: build.id,
38
+ gitSha: build.gitSha,
39
+ gitBranch: build.gitBranch,
40
+ authorName: build.authorName,
41
+ authorEmail: build.authorEmail,
42
+ message: build.message
43
+ };
44
+ }
45
+ export { REVIEW_EVENTS, announceReview, announceReviewWith, reviewRequestedData };
46
+
47
+ //# sourceMappingURL=review-events.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"review-events.mjs","names":[],"sources":["../src/review-events.ts"],"sourcesContent":["/**\n * Review events (ADR 0013, decisions 4 and 10; ADR 0014, decision 10). They ride the existing\n * paths: signed webhooks to the project's subscribers and the notification fan-out (stored\n * channels plus per-user subscriptions). There is no new transport.\n *\n * - `review:requested`: a build is waiting for a verdict. Default-on for the project's approvers\n * and admins (see `notify.ts`).\n * - `review:comment`: a build or story comment (snapshot comments keep `comment:created`).\n * - `review:verdict`: a reviewer approved or rejected a build.\n */\nimport { emitWebhookEvent } from \"@storyshelf/core/adapter/webhook-events\";\nimport type { Build, Project } from \"@storyshelf/core/schema\";\nimport { credentialKeys } from \"./credential-keys.ts\";\nimport { notifyProjectWith, type NotifyDeps } from \"./notify.ts\";\nimport { getStore } from \"./store.ts\";\n\n/** Event names of the review topics. */\nexport const REVIEW_EVENTS = {\n requested: \"review:requested\",\n comment: \"review:comment\",\n verdict: \"review:verdict\",\n} as const;\n\n/** Webhook and notification fan-out for one review event, from explicit dependencies. */\nexport async function announceReviewWith(\n deps: NotifyDeps,\n project: Pick<Project, \"id\" | \"slug\">,\n event: (typeof REVIEW_EVENTS)[keyof typeof REVIEW_EVENTS],\n data: Record<string, unknown>,\n buildId: string,\n): Promise<void> {\n await emitWebhookEvent(\n deps.db,\n { webhooks: deps.db.tables.webhooks },\n project.id,\n event,\n data,\n credentialKeys(deps.config),\n deps.logger,\n );\n await notifyProjectWith(deps, project, event, data, `/builds/${buildId}`);\n}\n\n/** {@link announceReviewWith} inside a request. */\nexport async function announceReview(\n project: Pick<Project, \"id\" | \"slug\">,\n event: (typeof REVIEW_EVENTS)[keyof typeof REVIEW_EVENTS],\n data: Record<string, unknown>,\n buildId: string,\n): Promise<void> {\n await announceReviewWith(getStore(), project, event, data, buildId);\n}\n\n/** What `review:requested` carries: the build to look at. */\nexport function reviewRequestedData(build: Build): Record<string, unknown> {\n return {\n buildId: build.id,\n gitSha: build.gitSha,\n gitBranch: build.gitBranch,\n authorName: build.authorName,\n authorEmail: build.authorEmail,\n message: build.message,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAiBA,MAAa,gBAAgB;CAC3B,WAAW;CACX,SAAS;CACT,SAAS;AACX;;AAGA,eAAsB,mBACpB,MACA,SACA,OACA,MACA,SACe;CACf,MAAM,iBACJ,KAAK,IACL,EAAE,UAAU,KAAK,GAAG,OAAO,SAAS,GACpC,QAAQ,IACR,OACA,MACA,eAAe,KAAK,MAAM,GAC1B,KAAK,MACP;CACA,MAAM,kBAAkB,MAAM,SAAS,OAAO,MAAM,WAAW,SAAS;AAC1E;;AAGA,eAAsB,eACpB,SACA,OACA,MACA,SACe;CACf,MAAM,mBAAmB,SAAS,GAAG,SAAS,OAAO,MAAM,OAAO;AACpE;;AAGA,SAAgB,oBAAoB,OAAuC;CACzE,OAAO;EACL,SAAS,MAAM;EACf,QAAQ,MAAM;EACd,WAAW,MAAM;EACjB,YAAY,MAAM;EAClB,aAAa,MAAM;EACnB,SAAS,MAAM;CACjB;AACF"}
@@ -2,18 +2,25 @@ import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
4
  import { getStore } from "../store.mjs";
5
+ import { notificationsPaused } from "../notifications-pause.mjs";
5
6
  import { loadCredentialProbe, reencryptWithCurrent } from "../credentials.mjs";
7
+ import { countHiddenSnapshots } from "../hidden-data.mjs";
8
+ import { requireSiteAdmin } from "./helpers.mjs";
9
+ import { insightDepsFromStore } from "../insights/deps.mjs";
10
+ import { buildUsageReport } from "../insights/usage-report.mjs";
11
+ import { renderAdminAiPage } from "../pages/admin-ai.mjs";
12
+ import { runnerTargets } from "../targets.mjs";
6
13
  import { renderAdminSystemPage } from "../pages/admin-system.mjs";
7
14
  import { hxRedirect } from "./htmx.mjs";
8
15
  import { flash } from "./flash.mjs";
9
16
  import { collectHealthReport } from "./health-report.mjs";
10
- import { requireSiteAdmin } from "./helpers.mjs";
11
- //#region src/routers/admin-pages.ts
17
+ import { describeCaptureHost } from "@storyshelf/core/utils";
18
+ // src/routers/admin-pages.ts
12
19
  /** Register the site-admin System page (adapter inventory + in-depth health). */
13
20
  function registerAdminPages(app, deps, auth) {
14
21
  app.get("/admin", async (c) => {
15
22
  requireSiteAdmin(c);
16
- const { authEnabled, config, db } = getStore();
23
+ const { authEnabled, config, db, capturesHere, captureConcurrency, testing } = getStore();
17
24
  const credentials = await loadCredentialProbe(db, config);
18
25
  const report = await collectHealthReport(deps.sources, deps.getSettled(), deps.bootTimeMs, deps.version, credentials);
19
26
  const authMethods = auth?.loginMethods().map((method) => method.label);
@@ -23,9 +30,21 @@ function registerAdminPages(app, deps, auth) {
23
30
  authEnabled,
24
31
  config,
25
32
  authMethods,
26
- credentials
33
+ credentials,
34
+ aiEnabled: getStore().ai !== void 0,
35
+ testing,
36
+ hiddenSnapshots: testing ? 0 : await countHiddenSnapshots(db),
37
+ ...await captureView(),
38
+ capture: capturesHere === true ? describeCaptureHost(captureConcurrency) : null
27
39
  }));
28
40
  });
41
+ app.get("/admin/ai", async (c) => {
42
+ requireSiteAdmin(c);
43
+ const aiDeps = insightDepsFromStore();
44
+ if (!aiDeps) return c.notFound();
45
+ c.header("Cache-Control", "no-store");
46
+ return c.html(renderAdminAiPage(await buildUsageReport(aiDeps)));
47
+ });
29
48
  app.post("/admin/credentials/reencrypt", async (c) => {
30
49
  requireSiteAdmin(c);
31
50
  const { db, config } = getStore();
@@ -42,7 +61,29 @@ function reencryptMessage(result) {
42
61
  const base = `Re-encrypted ${result.reencrypted} credential(s)`;
43
62
  return result.failed > 0 ? `${base}; ${result.failed} failed — see server logs` : base;
44
63
  }
45
- //#endregion
64
+ /** Whether the System page warns that notifications are paused. */
65
+ async function pausedView() {
66
+ const store = getStore();
67
+ return store.notifiers.length > 0 && await notificationsPaused(store);
68
+ }
69
+ /** The System page's capture browsers and public origin (ADR 0007). */
70
+ async function captureView() {
71
+ return {
72
+ targets: await runnerTargets(),
73
+ publicOrigin: await publicOriginView(),
74
+ notificationsPaused: await pausedView()
75
+ };
76
+ }
77
+ /** What the System page shows about the public origin: what resolves (confirmed), and what a browser reported. */
78
+ async function publicOriginView() {
79
+ const { publicOrigin, authEnabled } = getStore();
80
+ if (publicOrigin === void 0) return;
81
+ return {
82
+ resolved: await publicOrigin.resolve(),
83
+ stored: await publicOrigin.stored(),
84
+ learns: authEnabled
85
+ };
86
+ }
46
87
  export { registerAdminPages };
47
88
 
48
89
  //# sourceMappingURL=admin-pages.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"admin-pages.mjs","names":[],"sources":["../../src/routers/admin-pages.ts"],"sourcesContent":["import type { Auth } from \"@storyshelf/core/auth\";\nimport type { AdapterSetupResult, AdapterSetupSources } from \"@storyshelf/core/adapter/setup\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { loadCredentialProbe, reencryptWithCurrent } from \"../credentials.ts\";\nimport { renderAdminSystemPage } from \"../pages/admin-system.tsx\";\nimport { getStore } from \"../store.ts\";\nimport { flash } from \"./flash.ts\";\nimport { collectHealthReport } from \"./health-report.ts\";\nimport { requireSiteAdmin } from \"./helpers.ts\";\nimport { hxRedirect } from \"./htmx.ts\";\n\n/** Dependencies for the admin System page (same probing inputs as deep health). */\nexport interface AdminPageDeps {\n sources: AdapterSetupSources;\n getSettled: () => AdapterSetupResult | null;\n bootTimeMs: number;\n version: string;\n}\n\n/** Register the site-admin System page (adapter inventory + in-depth health). */\nexport function registerAdminPages(app: ShelfRouter, deps: AdminPageDeps, auth?: Auth): void {\n app.get(\"/admin\", async (c) => {\n requireSiteAdmin(c);\n const { authEnabled, config, db } = getStore();\n const credentials = await loadCredentialProbe(db, config);\n const report = await collectHealthReport(\n deps.sources,\n deps.getSettled(),\n deps.bootTimeMs,\n deps.version,\n credentials,\n );\n const authMethods = auth?.loginMethods().map((method) => method.label);\n c.header(\"Cache-Control\", \"no-store\");\n return c.html(renderAdminSystemPage({ report, authEnabled, config, authMethods, credentials }));\n });\n\n app.post(\"/admin/credentials/reencrypt\", async (c) => {\n requireSiteAdmin(c);\n const { db, config } = getStore();\n if (!config.previousSecret) {\n flash(c, \"No previous secret is configured\", \"warning\");\n return hxRedirect(c, \"/admin\");\n }\n const result = await reencryptWithCurrent(db, config);\n flash(c, reencryptMessage(result), result.failed > 0 ? \"warning\" : \"success\");\n return hxRedirect(c, \"/admin\");\n });\n}\n\nfunction reencryptMessage(result: { reencrypted: number; failed: number }): string {\n const base = `Re-encrypted ${result.reencrypted} credential(s)`;\n return result.failed > 0 ? `${base}; ${result.failed} failed — see server logs` : base;\n}\n"],"mappings":";;;;;;;;;;;;AAoBA,SAAgB,mBAAmB,KAAkB,MAAqB,MAAmB;CAC3F,IAAI,IAAI,UAAU,OAAO,MAAM;EAC7B,iBAAiB,CAAC;EAClB,MAAM,EAAE,aAAa,QAAQ,OAAO,SAAS;EAC7C,MAAM,cAAc,MAAM,oBAAoB,IAAI,MAAM;EACxD,MAAM,SAAS,MAAM,oBACnB,KAAK,SACL,KAAK,WAAW,GAChB,KAAK,YACL,KAAK,SACL,WACF;EACA,MAAM,cAAc,MAAM,aAAa,CAAC,CAAC,KAAK,WAAW,OAAO,KAAK;EACrE,EAAE,OAAO,iBAAiB,UAAU;EACpC,OAAO,EAAE,KAAK,sBAAsB;GAAE;GAAQ;GAAa;GAAQ;GAAa;EAAY,CAAC,CAAC;CAChG,CAAC;CAED,IAAI,KAAK,gCAAgC,OAAO,MAAM;EACpD,iBAAiB,CAAC;EAClB,MAAM,EAAE,IAAI,WAAW,SAAS;EAChC,IAAI,CAAC,OAAO,gBAAgB;GAC1B,MAAM,GAAG,oCAAoC,SAAS;GACtD,OAAO,WAAW,GAAG,QAAQ;EAC/B;EACA,MAAM,SAAS,MAAM,qBAAqB,IAAI,MAAM;EACpD,MAAM,GAAG,iBAAiB,MAAM,GAAG,OAAO,SAAS,IAAI,YAAY,SAAS;EAC5E,OAAO,WAAW,GAAG,QAAQ;CAC/B,CAAC;AACH;AAEA,SAAS,iBAAiB,QAAyD;CACjF,MAAM,OAAO,gBAAgB,OAAO,YAAY;CAChD,OAAO,OAAO,SAAS,IAAI,GAAG,KAAK,IAAI,OAAO,OAAO,6BAA6B;AACpF"}
1
+ {"version":3,"file":"admin-pages.mjs","names":[],"sources":["../../src/routers/admin-pages.ts"],"sourcesContent":["import type { AdapterSetupResult, AdapterSetupSources } from \"@storyshelf/core/adapter/setup\";\nimport type { Auth } from \"@storyshelf/core/auth\";\nimport { describeCaptureHost } from \"@storyshelf/core/utils\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { loadCredentialProbe, reencryptWithCurrent } from \"../credentials.ts\";\nimport { countHiddenSnapshots } from \"../hidden-data.ts\";\nimport { insightDepsFromStore } from \"../insights/deps.ts\";\nimport { buildUsageReport } from \"../insights/usage-report.ts\";\nimport { notificationsPaused } from \"../notifications-pause.ts\";\nimport { renderAdminAiPage } from \"../pages/admin-ai.tsx\";\nimport { renderAdminSystemPage, type AdminSystemData } from \"../pages/admin-system.tsx\";\nimport { getStore } from \"../store.ts\";\nimport { runnerTargets } from \"../targets.ts\";\nimport { flash } from \"./flash.ts\";\nimport { collectHealthReport } from \"./health-report.ts\";\nimport { requireSiteAdmin } from \"./helpers.ts\";\nimport { hxRedirect } from \"./htmx.ts\";\n\n/** Dependencies for the admin System page (same probing inputs as deep health). */\nexport interface AdminPageDeps {\n sources: AdapterSetupSources;\n getSettled: () => AdapterSetupResult | null;\n bootTimeMs: number;\n version: string;\n}\n\n/** Register the site-admin System page (adapter inventory + in-depth health). */\nexport function registerAdminPages(app: ShelfRouter, deps: AdminPageDeps, auth?: Auth): void {\n app.get(\"/admin\", async (c) => {\n requireSiteAdmin(c);\n const { authEnabled, config, db, capturesHere, captureConcurrency, testing } = getStore();\n const credentials = await loadCredentialProbe(db, config);\n const report = await collectHealthReport(\n deps.sources,\n deps.getSettled(),\n deps.bootTimeMs,\n deps.version,\n credentials,\n );\n const authMethods = auth?.loginMethods().map((method) => method.label);\n c.header(\"Cache-Control\", \"no-store\");\n return c.html(\n renderAdminSystemPage({\n report,\n authEnabled,\n config,\n authMethods,\n credentials,\n aiEnabled: getStore().ai !== undefined,\n testing,\n hiddenSnapshots: testing ? 0 : await countHiddenSnapshots(db),\n ...(await captureView()),\n // Only a host that runs capture jobs (one with `testing`) is sized here.\n capture: capturesHere === true ? describeCaptureHost(captureConcurrency) : null,\n }),\n );\n });\n\n app.get(\"/admin/ai\", async (c) => {\n requireSiteAdmin(c);\n const aiDeps = insightDepsFromStore();\n if (!aiDeps) {\n return c.notFound();\n }\n c.header(\"Cache-Control\", \"no-store\");\n return c.html(renderAdminAiPage(await buildUsageReport(aiDeps)));\n });\n\n app.post(\"/admin/credentials/reencrypt\", async (c) => {\n requireSiteAdmin(c);\n const { db, config } = getStore();\n if (!config.previousSecret) {\n flash(c, \"No previous secret is configured\", \"warning\");\n return hxRedirect(c, \"/admin\");\n }\n const result = await reencryptWithCurrent(db, config);\n flash(c, reencryptMessage(result), result.failed > 0 ? \"warning\" : \"success\");\n return hxRedirect(c, \"/admin\");\n });\n}\n\nfunction reencryptMessage(result: { reencrypted: number; failed: number }): string {\n const base = `Re-encrypted ${result.reencrypted} credential(s)`;\n return result.failed > 0 ? `${base}; ${result.failed} failed — see server logs` : base;\n}\n\n/** Whether the System page warns that notifications are paused. */\nasync function pausedView(): Promise<boolean> {\n const store = getStore();\n return store.notifiers.length > 0 && (await notificationsPaused(store));\n}\n\n/** The System page's capture browsers and public origin (ADR 0007). */\nasync function captureView(): Promise<\n Pick<AdminSystemData, \"targets\" | \"publicOrigin\" | \"notificationsPaused\">\n> {\n return {\n targets: await runnerTargets(),\n publicOrigin: await publicOriginView(),\n notificationsPaused: await pausedView(),\n };\n}\n\n/** What the System page shows about the public origin: what resolves (confirmed), and what a browser reported. */\nasync function publicOriginView(): Promise<AdminSystemData[\"publicOrigin\"]> {\n const { publicOrigin, authEnabled } = getStore();\n if (publicOrigin === undefined) {\n return undefined;\n }\n return {\n resolved: await publicOrigin.resolve(),\n stored: await publicOrigin.stored(),\n // Only a deployment with admins can learn an origin from one.\n learns: authEnabled,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AA2BA,SAAgB,mBAAmB,KAAkB,MAAqB,MAAmB;CAC3F,IAAI,IAAI,UAAU,OAAO,MAAM;EAC7B,iBAAiB,CAAC;EAClB,MAAM,EAAE,aAAa,QAAQ,IAAI,cAAc,oBAAoB,YAAY,SAAS;EACxF,MAAM,cAAc,MAAM,oBAAoB,IAAI,MAAM;EACxD,MAAM,SAAS,MAAM,oBACnB,KAAK,SACL,KAAK,WAAW,GAChB,KAAK,YACL,KAAK,SACL,WACF;EACA,MAAM,cAAc,MAAM,aAAa,CAAC,CAAC,KAAK,WAAW,OAAO,KAAK;EACrE,EAAE,OAAO,iBAAiB,UAAU;EACpC,OAAO,EAAE,KACP,sBAAsB;GACpB;GACA;GACA;GACA;GACA;GACA,WAAW,SAAS,CAAC,CAAC,OAAO,KAAA;GAC7B;GACA,iBAAiB,UAAU,IAAI,MAAM,qBAAqB,EAAE;GAC5D,GAAI,MAAM,YAAY;GAEtB,SAAS,iBAAiB,OAAO,oBAAoB,kBAAkB,IAAI;EAC7E,CAAC,CACH;CACF,CAAC;CAED,IAAI,IAAI,aAAa,OAAO,MAAM;EAChC,iBAAiB,CAAC;EAClB,MAAM,SAAS,qBAAqB;EACpC,IAAI,CAAC,QACH,OAAO,EAAE,SAAS;EAEpB,EAAE,OAAO,iBAAiB,UAAU;EACpC,OAAO,EAAE,KAAK,kBAAkB,MAAM,iBAAiB,MAAM,CAAC,CAAC;CACjE,CAAC;CAED,IAAI,KAAK,gCAAgC,OAAO,MAAM;EACpD,iBAAiB,CAAC;EAClB,MAAM,EAAE,IAAI,WAAW,SAAS;EAChC,IAAI,CAAC,OAAO,gBAAgB;GAC1B,MAAM,GAAG,oCAAoC,SAAS;GACtD,OAAO,WAAW,GAAG,QAAQ;EAC/B;EACA,MAAM,SAAS,MAAM,qBAAqB,IAAI,MAAM;EACpD,MAAM,GAAG,iBAAiB,MAAM,GAAG,OAAO,SAAS,IAAI,YAAY,SAAS;EAC5E,OAAO,WAAW,GAAG,QAAQ;CAC/B,CAAC;AACH;AAEA,SAAS,iBAAiB,QAAyD;CACjF,MAAM,OAAO,gBAAgB,OAAO,YAAY;CAChD,OAAO,OAAO,SAAS,IAAI,GAAG,KAAK,IAAI,OAAO,OAAO,6BAA6B;AACpF;;AAGA,eAAe,aAA+B;CAC5C,MAAM,QAAQ,SAAS;CACvB,OAAO,MAAM,UAAU,SAAS,KAAM,MAAM,oBAAoB,KAAK;AACvE;;AAGA,eAAe,cAEb;CACA,OAAO;EACL,SAAS,MAAM,cAAc;EAC7B,cAAc,MAAM,iBAAiB;EACrC,qBAAqB,MAAM,WAAW;CACxC;AACF;;AAGA,eAAe,mBAA6D;CAC1E,MAAM,EAAE,cAAc,gBAAgB,SAAS;CAC/C,IAAI,iBAAiB,KAAA,GACnB;CAEF,OAAO;EACL,UAAU,MAAM,aAAa,QAAQ;EACrC,QAAQ,MAAM,aAAa,OAAO;EAElC,QAAQ;CACV;AACF"}
@@ -0,0 +1,92 @@
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 { requestOpenBuildStatesNow } from "../git-status.mjs";
9
+ import { normalizeOrigin } from "@storyshelf/core/utils";
10
+ import { HTTPException } from "hono/http-exception";
11
+ // src/routers/admin-public-origin.ts
12
+ /**
13
+ * The public origin, learnt from an admin's browser (ADR 0007, decision 6): the System page reports
14
+ * `location.origin`, an admin confirms or replaces it, and background jobs read it from the database.
15
+ * Site-admin and CSRF protected; with authentication off there is no admin, so nothing is learnt.
16
+ */
17
+ /** Register the public-origin routes of the System page. */
18
+ function registerPublicOriginRoutes(app) {
19
+ app.post("/admin/public-origin/report", handleReport);
20
+ app.post("/admin/public-origin", handleSet);
21
+ app.post("/admin/public-origin/clear", handleClear);
22
+ }
23
+ /** What an admin is told when the origin is not theirs to set. */
24
+ const FIXED_ORIGIN_MESSAGE = "The public origin comes from PUBLIC_BASE_URL or the hosting platform, so there is nothing to confirm";
25
+ /** Whether the configuration or the platform answers: an origin an admin cannot confirm or replace. */
26
+ async function isFixed() {
27
+ const source = (await getStore().publicOrigin?.resolve())?.source;
28
+ return source === "config" || source === "platform";
29
+ }
30
+ /** The origin a report names, once it matches what the browser says it used; throws 400 otherwise. */
31
+ async function reportedOrigin(c) {
32
+ const body = await c.req.json().catch(() => null);
33
+ const reported = typeof body?.origin === "string" ? normalizeOrigin(body.origin) : void 0;
34
+ if (reported === void 0 || c.req.header("origin") !== reported) throw new HTTPException(400, { message: "Origin does not match the request" });
35
+ return reported;
36
+ }
37
+ /** The page reports the origin it was loaded from; the first report is kept. */
38
+ async function handleReport(c) {
39
+ requireSiteAdmin(c);
40
+ const { authEnabled, publicOrigin, user } = getStore();
41
+ if (!authEnabled || publicOrigin === void 0 || await isFixed()) return c.body(null, 204);
42
+ const reported = await reportedOrigin(c);
43
+ const before = await publicOrigin.stored();
44
+ const wasResolved = await publicOrigin.url() !== void 0;
45
+ const stored = await publicOrigin.report(reported, user?.id);
46
+ await resumeIfResolved(wasResolved);
47
+ return c.json(stored, before === null ? 201 : 200);
48
+ }
49
+ /**
50
+ * Notifications were paused and an origin now resolves (an admin confirmed one; a mere report
51
+ * resolves nothing): re-request the state of open builds.
52
+ */
53
+ async function resumeIfResolved(wasResolved) {
54
+ if (!wasResolved && await getStore().publicOrigin?.url() !== void 0) requestOpenBuildStatesNow().catch(() => 0);
55
+ }
56
+ /** Store what an admin typed; the message says why not when it is no origin. */
57
+ async function storeTyped(c) {
58
+ if (await isFixed()) return FIXED_ORIGIN_MESSAGE;
59
+ const { publicOrigin, user } = getStore();
60
+ const origin = (await c.req.formData()).get("origin");
61
+ try {
62
+ const wasResolved = await publicOrigin?.url() !== void 0;
63
+ await publicOrigin?.set(typeof origin === "string" ? origin : "", user?.id);
64
+ await resumeIfResolved(wasResolved);
65
+ return "Public origin saved";
66
+ } catch (error) {
67
+ return error instanceof Error ? error.message : "Invalid origin";
68
+ }
69
+ }
70
+ /** An admin confirmed the detected origin or entered another. */
71
+ async function handleSet(c) {
72
+ requireSiteAdmin(c);
73
+ const { authEnabled, publicOrigin } = getStore();
74
+ if (!authEnabled || publicOrigin === void 0) {
75
+ flash(c, "The public origin is learnt from an admin's browser, which needs authentication", "warning");
76
+ return hxRedirect(c, "/admin");
77
+ }
78
+ const message = await storeTyped(c);
79
+ flash(c, message, message === "Public origin saved" ? "success" : "warning");
80
+ return hxRedirect(c, "/admin");
81
+ }
82
+ /** An admin dropped the stored origin; the next admin visit reports a fresh one. */
83
+ async function handleClear(c) {
84
+ requireSiteAdmin(c);
85
+ const { publicOrigin } = getStore();
86
+ await publicOrigin?.clear();
87
+ flash(c, "Stored public origin removed");
88
+ return hxRedirect(c, "/admin");
89
+ }
90
+ export { registerPublicOriginRoutes };
91
+
92
+ //# sourceMappingURL=admin-public-origin.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"admin-public-origin.mjs","names":[],"sources":["../../src/routers/admin-public-origin.ts"],"sourcesContent":["/**\n * The public origin, learnt from an admin's browser (ADR 0007, decision 6): the System page reports\n * `location.origin`, an admin confirms or replaces it, and background jobs read it from the database.\n * Site-admin and CSRF protected; with authentication off there is no admin, so nothing is learnt.\n */\nimport { normalizeOrigin } from \"@storyshelf/core/utils\";\nimport type { Context } from \"hono\";\nimport { HTTPException } from \"hono/http-exception\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { requestOpenBuildStatesNow } from \"../git-status.ts\";\nimport { getStore } from \"../store.ts\";\nimport { flash } from \"./flash.ts\";\nimport { requireSiteAdmin } from \"./helpers.ts\";\nimport { hxRedirect } from \"./htmx.ts\";\n\n/** Register the public-origin routes of the System page. */\nexport function registerPublicOriginRoutes(app: ShelfRouter): void {\n app.post(\"/admin/public-origin/report\", handleReport);\n app.post(\"/admin/public-origin\", handleSet);\n app.post(\"/admin/public-origin/clear\", handleClear);\n}\n\n/** What an admin is told when the origin is not theirs to set. */\nconst FIXED_ORIGIN_MESSAGE =\n \"The public origin comes from PUBLIC_BASE_URL or the hosting platform, so there is nothing to confirm\";\n\n/** Whether the configuration or the platform answers: an origin an admin cannot confirm or replace. */\nasync function isFixed(): Promise<boolean> {\n const source = (await getStore().publicOrigin?.resolve())?.source;\n return source === \"config\" || source === \"platform\";\n}\n\n/** The origin a report names, once it matches what the browser says it used; throws 400 otherwise. */\nasync function reportedOrigin(c: Context): Promise<string> {\n const body = (await c.req.json().catch(() => null)) as { origin?: unknown } | null;\n const reported = typeof body?.origin === \"string\" ? normalizeOrigin(body.origin) : undefined;\n // The browser states the origin it used itself; a report that disagrees is not one.\n if (reported === undefined || c.req.header(\"origin\") !== reported) {\n throw new HTTPException(400, { message: \"Origin does not match the request\" });\n }\n return reported;\n}\n\n/** The page reports the origin it was loaded from; the first report is kept. */\nasync function handleReport(c: Context): Promise<Response> {\n requireSiteAdmin(c);\n const { authEnabled, publicOrigin, user } = getStore();\n if (!authEnabled || publicOrigin === undefined || (await isFixed())) {\n return c.body(null, 204);\n }\n const reported = await reportedOrigin(c);\n const before = await publicOrigin.stored();\n const wasResolved = (await publicOrigin.url()) !== undefined;\n const stored = await publicOrigin.report(reported, user?.id);\n await resumeIfResolved(wasResolved);\n return c.json(stored, before === null ? 201 : 200);\n}\n\n/**\n * Notifications were paused and an origin now resolves (an admin confirmed one; a mere report\n * resolves nothing): re-request the state of open builds.\n */\nasync function resumeIfResolved(wasResolved: boolean): Promise<void> {\n if (!wasResolved && (await getStore().publicOrigin?.url()) !== undefined) {\n requestOpenBuildStatesNow().catch(() => 0);\n }\n}\n\n/** Store what an admin typed; the message says why not when it is no origin. */\nasync function storeTyped(c: Context): Promise<string> {\n if (await isFixed()) {\n return FIXED_ORIGIN_MESSAGE;\n }\n const { publicOrigin, user } = getStore();\n const origin = (await c.req.formData()).get(\"origin\");\n try {\n const wasResolved = (await publicOrigin?.url()) !== undefined;\n await publicOrigin?.set(typeof origin === \"string\" ? origin : \"\", user?.id);\n await resumeIfResolved(wasResolved);\n return \"Public origin saved\";\n } catch (error) {\n return error instanceof Error ? error.message : \"Invalid origin\";\n }\n}\n\n/** An admin confirmed the detected origin or entered another. */\nasync function handleSet(c: Context): Promise<Response> {\n requireSiteAdmin(c);\n const { authEnabled, publicOrigin } = getStore();\n if (!authEnabled || publicOrigin === undefined) {\n flash(\n c,\n \"The public origin is learnt from an admin's browser, which needs authentication\",\n \"warning\",\n );\n return hxRedirect(c, \"/admin\");\n }\n const message = await storeTyped(c);\n flash(c, message, message === \"Public origin saved\" ? \"success\" : \"warning\");\n return hxRedirect(c, \"/admin\");\n}\n\n/** An admin dropped the stored origin; the next admin visit reports a fresh one. */\nasync function handleClear(c: Context): Promise<Response> {\n requireSiteAdmin(c);\n const { publicOrigin } = getStore();\n await publicOrigin?.clear();\n flash(c, \"Stored public origin removed\");\n return hxRedirect(c, \"/admin\");\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAgBA,SAAgB,2BAA2B,KAAwB;CACjE,IAAI,KAAK,+BAA+B,YAAY;CACpD,IAAI,KAAK,wBAAwB,SAAS;CAC1C,IAAI,KAAK,8BAA8B,WAAW;AACpD;;AAGA,MAAM,uBACJ;;AAGF,eAAe,UAA4B;CACzC,MAAM,UAAU,MAAM,SAAS,CAAC,CAAC,cAAc,QAAQ,EAAA,EAAI;CAC3D,OAAO,WAAW,YAAY,WAAW;AAC3C;;AAGA,eAAe,eAAe,GAA6B;CACzD,MAAM,OAAQ,MAAM,EAAE,IAAI,KAAK,CAAC,CAAC,YAAY,IAAI;CACjD,MAAM,WAAW,OAAO,MAAM,WAAW,WAAW,gBAAgB,KAAK,MAAM,IAAI,KAAA;CAEnF,IAAI,aAAa,KAAA,KAAa,EAAE,IAAI,OAAO,QAAQ,MAAM,UACvD,MAAM,IAAI,cAAc,KAAK,EAAE,SAAS,oCAAoC,CAAC;CAE/E,OAAO;AACT;;AAGA,eAAe,aAAa,GAA+B;CACzD,iBAAiB,CAAC;CAClB,MAAM,EAAE,aAAa,cAAc,SAAS,SAAS;CACrD,IAAI,CAAC,eAAe,iBAAiB,KAAA,KAAc,MAAM,QAAQ,GAC/D,OAAO,EAAE,KAAK,MAAM,GAAG;CAEzB,MAAM,WAAW,MAAM,eAAe,CAAC;CACvC,MAAM,SAAS,MAAM,aAAa,OAAO;CACzC,MAAM,cAAe,MAAM,aAAa,IAAI,MAAO,KAAA;CACnD,MAAM,SAAS,MAAM,aAAa,OAAO,UAAU,MAAM,EAAE;CAC3D,MAAM,iBAAiB,WAAW;CAClC,OAAO,EAAE,KAAK,QAAQ,WAAW,OAAO,MAAM,GAAG;AACnD;;;;;AAMA,eAAe,iBAAiB,aAAqC;CACnE,IAAI,CAAC,eAAgB,MAAM,SAAS,CAAC,CAAC,cAAc,IAAI,MAAO,KAAA,GAC7D,0BAA0B,CAAC,CAAC,YAAY,CAAC;AAE7C;;AAGA,eAAe,WAAW,GAA6B;CACrD,IAAI,MAAM,QAAQ,GAChB,OAAO;CAET,MAAM,EAAE,cAAc,SAAS,SAAS;CACxC,MAAM,UAAU,MAAM,EAAE,IAAI,SAAS,EAAA,CAAG,IAAI,QAAQ;CACpD,IAAI;EACF,MAAM,cAAe,MAAM,cAAc,IAAI,MAAO,KAAA;EACpD,MAAM,cAAc,IAAI,OAAO,WAAW,WAAW,SAAS,IAAI,MAAM,EAAE;EAC1E,MAAM,iBAAiB,WAAW;EAClC,OAAO;CACT,SAAS,OAAO;EACd,OAAO,iBAAiB,QAAQ,MAAM,UAAU;CAClD;AACF;;AAGA,eAAe,UAAU,GAA+B;CACtD,iBAAiB,CAAC;CAClB,MAAM,EAAE,aAAa,iBAAiB,SAAS;CAC/C,IAAI,CAAC,eAAe,iBAAiB,KAAA,GAAW;EAC9C,MACE,GACA,mFACA,SACF;EACA,OAAO,WAAW,GAAG,QAAQ;CAC/B;CACA,MAAM,UAAU,MAAM,WAAW,CAAC;CAClC,MAAM,GAAG,SAAS,YAAY,wBAAwB,YAAY,SAAS;CAC3E,OAAO,WAAW,GAAG,QAAQ;AAC/B;;AAGA,eAAe,YAAY,GAA+B;CACxD,iBAAiB,CAAC;CAClB,MAAM,EAAE,iBAAiB,SAAS;CAClC,MAAM,cAAc,MAAM;CAC1B,MAAM,GAAG,8BAA8B;CACvC,OAAO,WAAW,GAAG,QAAQ;AAC/B"}
@@ -5,11 +5,11 @@ import { getStore } from "../store.mjs";
5
5
  import { notifySystem } from "../notify.mjs";
6
6
  import { reencryptWithCurrent } from "../credentials.mjs";
7
7
  import { requireSiteAdmin } from "./helpers.mjs";
8
- import { forbidden, purgeInputSchema, purgeSchema } from "./schemas.mjs";
8
+ import { errorSchema, forbidden, purgeInputSchema, purgeSchema } from "./schemas.mjs";
9
9
  import { createRoute, z } from "@hono/zod-openapi";
10
10
  import { ProjectModel } from "@storyshelf/core/models";
11
11
  import { Retention } from "@storyshelf/core/retention";
12
- //#region src/routers/admin.ts
12
+ // src/routers/admin.ts
13
13
  const purgeRoute = createRoute({
14
14
  method: "post",
15
15
  tags: ["Admin"],
@@ -21,6 +21,10 @@ const purgeRoute = createRoute({
21
21
  content: { "application/json": { schema: purgeSchema } },
22
22
  description: "Retention purge run"
23
23
  },
24
+ 503: {
25
+ content: { "application/json": { schema: errorSchema } },
26
+ description: "Testing retention requires atomic build lifecycle support"
27
+ },
24
28
  ...forbidden
25
29
  }
26
30
  });
@@ -61,6 +65,7 @@ function registerAdmin(app) {
61
65
  });
62
66
  app.openapi(purgeRoute, async (c) => {
63
67
  requireSiteAdmin(c);
68
+ if (getStore().testing) return c.json({ message: "Build purge is unavailable for testing deployments until the database adapter supports safe baseline retention. Builds are retained; contact your administrator." }, 503);
64
69
  const ttlDays = c.req.valid("json").ttlDays ?? getStore().config.purgeTtlDays ?? 30;
65
70
  const removedBuilds = await purgeBuilds(ttlDays);
66
71
  const branch = await purgeBranches();
@@ -74,7 +79,7 @@ function registerAdmin(app) {
74
79
  removedBuilds,
75
80
  removedBranches: branch.removedBranches,
76
81
  removedBaselines: branch.removedBaselines
77
- });
82
+ }, 200);
78
83
  });
79
84
  }
80
85
  async function purgeBuilds(ttlDays) {
@@ -107,7 +112,6 @@ async function purgeBranches() {
107
112
  removedBaselines: results.reduce((sum, r) => sum + r.removedBaselines, 0)
108
113
  };
109
114
  }
110
- //#endregion
111
115
  export { registerAdmin };
112
116
 
113
117
  //# sourceMappingURL=admin.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"admin.mjs","names":["forbiddenResponse"],"sources":["../../src/routers/admin.ts"],"sourcesContent":["/* oxlint-disable typescript/promise-function-async -- admin helpers are async by design */\nimport { createRoute, z } from \"@hono/zod-openapi\";\nimport { ProjectModel } from \"@storyshelf/core/models\";\nimport { Retention } from \"@storyshelf/core/retention\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { reencryptWithCurrent } from \"../credentials.ts\";\nimport { notifySystem } from \"../notify.ts\";\nimport { getStore } from \"../store.ts\";\nimport { requireSiteAdmin } from \"./helpers.ts\";\nimport { forbidden as forbiddenResponse, purgeInputSchema, purgeSchema } from \"./schemas.ts\";\nconst purgeRoute = createRoute({\n method: \"post\",\n tags: [\"Admin\"],\n summary: \"Run the retention purge\",\n path: \"/api/v1/admin/purge\",\n request: { body: { content: { \"application/json\": { schema: purgeInputSchema } } } },\n responses: {\n 200: {\n content: { \"application/json\": { schema: purgeSchema } },\n description: \"Retention purge run\",\n },\n ...forbiddenResponse,\n },\n});\n\nconst reencryptSchema = z.object({\n reencrypted: z.number().int(),\n failed: z.number().int(),\n unreadable: z.number().int(),\n});\n\nconst reencryptRoute = createRoute({\n method: \"post\",\n path: \"/api/v1/admin/credentials/reencrypt\",\n tags: [\"Admin\"],\n summary: \"Re-encrypt stored credentials with the current secret\",\n responses: {\n 200: {\n content: { \"application/json\": { schema: reencryptSchema } },\n description: \"Credentials re-encrypted with the current secret\",\n },\n 409: {\n content: { \"application/json\": { schema: z.object({ error: z.string() }) } },\n description: \"No previousSecret configured\",\n },\n ...forbiddenResponse,\n },\n});\n\n/** Register the site-admin endpoints: retention purge and credential re-encryption. */\nexport function registerAdmin(app: ShelfRouter): void {\n app.openapi(reencryptRoute, async (c) => {\n requireSiteAdmin(c);\n const { db, config } = getStore();\n if (!config.previousSecret) {\n return c.json({ error: \"No previousSecret is configured; nothing to re-encrypt\" }, 409);\n }\n const result = await reencryptWithCurrent(db, config);\n return c.json(\n {\n reencrypted: result.reencrypted,\n failed: result.failed,\n unreadable: result.unreadable.length,\n },\n 200,\n );\n });\n\n app.openapi(purgeRoute, async (c) => {\n requireSiteAdmin(c);\n const body = c.req.valid(\"json\");\n const ttlDays = body.ttlDays ?? getStore().config.purgeTtlDays ?? 30;\n const removedBuilds = await purgeBuilds(ttlDays);\n const branch = await purgeBranches();\n await notifySystem(\"sys:purge-completed\", {\n removedBuilds,\n removedBranches: branch.removedBranches,\n removedBaselines: branch.removedBaselines,\n ttlDays,\n });\n return c.json({\n removedBuilds,\n removedBranches: branch.removedBranches,\n removedBaselines: branch.removedBaselines,\n });\n });\n}\n\n// oxlint-disable-next-line typescript/promise-function-async\nasync function purgeBuilds(ttlDays: number): Promise<number> {\n const projects = await new ProjectModel(getStore().db).list();\n const retention = new Retention(\n getStore().db,\n getStore().storage,\n {\n builds: getStore().db.tables.builds,\n buildLabels: getStore().db.tables.buildLabels,\n baselines: getStore().db.tables.baselines,\n },\n getStore().logger,\n );\n const results = await Promise.all(\n projects.map((project) => retention.purge(project, { ttlDays, keepLatestPerBranch: true })),\n );\n return results.reduce((sum, result) => sum + result.removedBuilds, 0);\n}\n\n// oxlint-disable-next-line typescript/promise-function-async\nasync function purgeBranches(): Promise<{ removedBranches: number; removedBaselines: number }> {\n const branchTtl = getStore().config.branchTtlDays ?? 30;\n if (branchTtl === null) {\n return { removedBranches: 0, removedBaselines: 0 };\n }\n const projects = await new ProjectModel(getStore().db).list();\n const retention = new Retention(\n getStore().db,\n getStore().storage,\n {\n builds: getStore().db.tables.builds,\n buildLabels: getStore().db.tables.buildLabels,\n baselines: getStore().db.tables.baselines,\n },\n getStore().logger,\n );\n const results = await Promise.all(\n projects.map((project) => retention.purgeStaleBranches(project, branchTtl)),\n );\n const removedBranches = results.reduce(\n (sum: number, r: { removedBranches: number }) => sum + r.removedBranches,\n 0,\n );\n const removedBaselines = results.reduce(\n (sum: number, r: { removedBaselines: number }) => sum + r.removedBaselines,\n 0,\n );\n return { removedBranches, removedBaselines };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"admin.mjs","names":["forbiddenResponse"],"sources":["../../src/routers/admin.ts"],"sourcesContent":["/* oxlint-disable typescript/promise-function-async -- admin helpers are async by design */\nimport { createRoute, z } from \"@hono/zod-openapi\";\nimport { ProjectModel } from \"@storyshelf/core/models\";\nimport { Retention } from \"@storyshelf/core/retention\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { reencryptWithCurrent } from \"../credentials.ts\";\nimport { notifySystem } from \"../notify.ts\";\nimport { getStore } from \"../store.ts\";\nimport { requireSiteAdmin } from \"./helpers.ts\";\nimport {\n errorSchema,\n forbidden as forbiddenResponse,\n purgeInputSchema,\n purgeSchema,\n} from \"./schemas.ts\";\nconst purgeRoute = createRoute({\n method: \"post\",\n tags: [\"Admin\"],\n summary: \"Run the retention purge\",\n path: \"/api/v1/admin/purge\",\n request: { body: { content: { \"application/json\": { schema: purgeInputSchema } } } },\n responses: {\n 200: {\n content: { \"application/json\": { schema: purgeSchema } },\n description: \"Retention purge run\",\n },\n 503: {\n content: { \"application/json\": { schema: errorSchema } },\n description: \"Testing retention requires atomic build lifecycle support\",\n },\n ...forbiddenResponse,\n },\n});\n\nconst reencryptSchema = z.object({\n reencrypted: z.number().int(),\n failed: z.number().int(),\n unreadable: z.number().int(),\n});\n\nconst reencryptRoute = createRoute({\n method: \"post\",\n path: \"/api/v1/admin/credentials/reencrypt\",\n tags: [\"Admin\"],\n summary: \"Re-encrypt stored credentials with the current secret\",\n responses: {\n 200: {\n content: { \"application/json\": { schema: reencryptSchema } },\n description: \"Credentials re-encrypted with the current secret\",\n },\n 409: {\n content: { \"application/json\": { schema: z.object({ error: z.string() }) } },\n description: \"No previousSecret configured\",\n },\n ...forbiddenResponse,\n },\n});\n\n/** Register the site-admin endpoints: retention purge and credential re-encryption. */\nexport function registerAdmin(app: ShelfRouter): void {\n app.openapi(reencryptRoute, async (c) => {\n requireSiteAdmin(c);\n const { db, config } = getStore();\n if (!config.previousSecret) {\n return c.json({ error: \"No previousSecret is configured; nothing to re-encrypt\" }, 409);\n }\n const result = await reencryptWithCurrent(db, config);\n return c.json(\n {\n reencrypted: result.reencrypted,\n failed: result.failed,\n unreadable: result.unreadable.length,\n },\n 200,\n );\n });\n\n app.openapi(purgeRoute, async (c) => {\n requireSiteAdmin(c);\n // Until the database provides atomic reference admission and tombstones, deletion in a\n // testing deployment can race a baseline promotion. Do not expose unsafe purge.\n if (getStore().testing) {\n return c.json(\n {\n message:\n \"Build purge is unavailable for testing deployments until the database adapter supports safe baseline retention. Builds are retained; contact your administrator.\",\n },\n 503,\n );\n }\n const body = c.req.valid(\"json\");\n const ttlDays = body.ttlDays ?? getStore().config.purgeTtlDays ?? 30;\n const removedBuilds = await purgeBuilds(ttlDays);\n const branch = await purgeBranches();\n await notifySystem(\"sys:purge-completed\", {\n removedBuilds,\n removedBranches: branch.removedBranches,\n removedBaselines: branch.removedBaselines,\n ttlDays,\n });\n return c.json(\n {\n removedBuilds,\n removedBranches: branch.removedBranches,\n removedBaselines: branch.removedBaselines,\n },\n 200,\n );\n });\n}\n\n// oxlint-disable-next-line typescript/promise-function-async\nasync function purgeBuilds(ttlDays: number): Promise<number> {\n const projects = await new ProjectModel(getStore().db).list();\n const retention = new Retention(\n getStore().db,\n getStore().storage,\n {\n builds: getStore().db.tables.builds,\n buildLabels: getStore().db.tables.buildLabels,\n baselines: getStore().db.tables.baselines,\n },\n getStore().logger,\n );\n const results = await Promise.all(\n projects.map((project) => retention.purge(project, { ttlDays, keepLatestPerBranch: true })),\n );\n return results.reduce((sum, result) => sum + result.removedBuilds, 0);\n}\n\n// oxlint-disable-next-line typescript/promise-function-async\nasync function purgeBranches(): Promise<{ removedBranches: number; removedBaselines: number }> {\n const branchTtl = getStore().config.branchTtlDays ?? 30;\n if (branchTtl === null) {\n return { removedBranches: 0, removedBaselines: 0 };\n }\n const projects = await new ProjectModel(getStore().db).list();\n const retention = new Retention(\n getStore().db,\n getStore().storage,\n {\n builds: getStore().db.tables.builds,\n buildLabels: getStore().db.tables.buildLabels,\n baselines: getStore().db.tables.baselines,\n },\n getStore().logger,\n );\n const results = await Promise.all(\n projects.map((project) => retention.purgeStaleBranches(project, branchTtl)),\n );\n const removedBranches = results.reduce(\n (sum: number, r: { removedBranches: number }) => sum + r.removedBranches,\n 0,\n );\n const removedBaselines = results.reduce(\n (sum: number, r: { removedBaselines: number }) => sum + r.removedBaselines,\n 0,\n );\n return { removedBranches, removedBaselines };\n}\n"],"mappings":"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"}
@@ -2,7 +2,7 @@ import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
4
  import { assetManifest } from "../asset-manifest.mjs";
5
- //#region src/routers/assets.ts
5
+ // src/routers/assets.ts
6
6
  const IMMUTABLE = "public, max-age=31536000, immutable";
7
7
  /**
8
8
  * Serve a content-addressed asset. A matching hash is cacheable forever and
@@ -26,9 +26,9 @@ function serveHashed(c, asset, requestedHash) {
26
26
  etag
27
27
  });
28
28
  }
29
- /** Register the static-asset routes (HTMX bundle and icon sprite). */
29
+ /** Register the static-asset routes (HTMX bundle, shell script, icon sprite and mark). */
30
30
  function registerAssets(app) {
31
- const { htmx, icons, mark } = assetManifest;
31
+ const { client, htmx, icons, mark } = assetManifest;
32
32
  app.get(String.raw`/assets/:file{htmx-[a-f0-9]+\.js}`, (c) => {
33
33
  const hash = (c.req.param("file") ?? "").replaceAll(/^htmx-|\.js$/gu, "");
34
34
  return serveHashed(c, {
@@ -36,6 +36,13 @@ function registerAssets(app) {
36
36
  contentType: "application/javascript"
37
37
  }, hash);
38
38
  });
39
+ app.get(String.raw`/assets/:file{client-[a-f0-9]+\.js}`, (c) => {
40
+ const hash = (c.req.param("file") ?? "").replaceAll(/^client-|\.js$/gu, "");
41
+ return serveHashed(c, {
42
+ ...client,
43
+ contentType: "application/javascript"
44
+ }, hash);
45
+ });
39
46
  app.get(String.raw`/assets/:file{icons-[a-f0-9]+\.svg}`, (c) => {
40
47
  const hash = (c.req.param("file") ?? "").replaceAll(/^icons-|\.svg$/gu, "");
41
48
  return serveHashed(c, {
@@ -58,7 +65,6 @@ function registerAssets(app) {
58
65
  });
59
66
  });
60
67
  }
61
- //#endregion
62
68
  export { registerAssets };
63
69
 
64
70
  //# sourceMappingURL=assets.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"assets.mjs","names":[],"sources":["../../src/routers/assets.ts"],"sourcesContent":["import type { Context } from \"hono\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { assetManifest } from \"../asset-manifest.ts\";\n\nconst IMMUTABLE = \"public, max-age=31536000, immutable\";\n\ninterface Asset {\n source: string;\n hash: string;\n contentType: string;\n}\n\n/**\n * Serve a content-addressed asset. A matching hash is cacheable forever and\n * answers conditional requests with 304; a stale hash (an old page still\n * open after an upgrade) gets the current content but is never cached under\n * the old URL.\n */\nfunction serveHashed(c: Context, asset: Asset, requestedHash: string): Response {\n const etag = `\"${asset.hash}\"`;\n if (requestedHash !== asset.hash) {\n return c.body(asset.source, 200, {\n \"content-type\": asset.contentType,\n \"cache-control\": \"no-store\",\n });\n }\n if (c.req.header(\"if-none-match\") === etag) {\n return c.body(null, 304, { etag, \"cache-control\": IMMUTABLE });\n }\n return c.body(asset.source, 200, {\n \"content-type\": asset.contentType,\n \"cache-control\": IMMUTABLE,\n etag,\n });\n}\n\n/** Register the static-asset routes (HTMX bundle and icon sprite). */\nexport function registerAssets(app: ShelfRouter): void {\n const { htmx, icons, mark } = assetManifest;\n app.get(String.raw`/assets/:file{htmx-[a-f0-9]+\\.js}`, (c) => {\n const hash = (c.req.param(\"file\") ?? \"\").replaceAll(/^htmx-|\\.js$/gu, \"\");\n return serveHashed(c, { ...htmx, contentType: \"application/javascript\" }, hash);\n });\n app.get(String.raw`/assets/:file{icons-[a-f0-9]+\\.svg}`, (c) => {\n const hash = (c.req.param(\"file\") ?? \"\").replaceAll(/^icons-|\\.svg$/gu, \"\");\n return serveHashed(c, { ...icons, contentType: \"image/svg+xml\" }, hash);\n });\n app.get(String.raw`/assets/:file{mark-[a-f0-9]+\\.svg}`, (c) => {\n const hash = (c.req.param(\"file\") ?? \"\").replaceAll(/^mark-|\\.svg$/gu, \"\");\n return serveHashed(c, { ...mark, contentType: \"image/svg+xml\" }, hash);\n });\n // Legacy unversioned URL: kept for old pages and external embeds, but\n // revalidated (never immutable) because its content can change.\n app.get(\"/assets/htmx.js\", (c) => {\n return c.body(htmx.source, 200, {\n \"content-type\": \"application/javascript\",\n \"cache-control\": \"no-cache\",\n etag: `\"${htmx.hash}\"`,\n });\n });\n}\n"],"mappings":";;;;;AAIA,MAAM,YAAY;;;;;;;AAclB,SAAS,YAAY,GAAY,OAAc,eAAiC;CAC9E,MAAM,OAAO,IAAI,MAAM,KAAK;CAC5B,IAAI,kBAAkB,MAAM,MAC1B,OAAO,EAAE,KAAK,MAAM,QAAQ,KAAK;EAC/B,gBAAgB,MAAM;EACtB,iBAAiB;CACnB,CAAC;CAEH,IAAI,EAAE,IAAI,OAAO,eAAe,MAAM,MACpC,OAAO,EAAE,KAAK,MAAM,KAAK;EAAE;EAAM,iBAAiB;CAAU,CAAC;CAE/D,OAAO,EAAE,KAAK,MAAM,QAAQ,KAAK;EAC/B,gBAAgB,MAAM;EACtB,iBAAiB;EACjB;CACF,CAAC;AACH;;AAGA,SAAgB,eAAe,KAAwB;CACrD,MAAM,EAAE,MAAM,OAAO,SAAS;CAC9B,IAAI,IAAI,OAAO,GAAG,sCAAsC,MAAM;EAC5D,MAAM,QAAQ,EAAE,IAAI,MAAM,MAAM,KAAK,GAAA,CAAI,WAAW,kBAAkB,EAAE;EACxE,OAAO,YAAY,GAAG;GAAE,GAAG;GAAM,aAAa;EAAyB,GAAG,IAAI;CAChF,CAAC;CACD,IAAI,IAAI,OAAO,GAAG,wCAAwC,MAAM;EAC9D,MAAM,QAAQ,EAAE,IAAI,MAAM,MAAM,KAAK,GAAA,CAAI,WAAW,oBAAoB,EAAE;EAC1E,OAAO,YAAY,GAAG;GAAE,GAAG;GAAO,aAAa;EAAgB,GAAG,IAAI;CACxE,CAAC;CACD,IAAI,IAAI,OAAO,GAAG,uCAAuC,MAAM;EAC7D,MAAM,QAAQ,EAAE,IAAI,MAAM,MAAM,KAAK,GAAA,CAAI,WAAW,mBAAmB,EAAE;EACzE,OAAO,YAAY,GAAG;GAAE,GAAG;GAAM,aAAa;EAAgB,GAAG,IAAI;CACvE,CAAC;CAGD,IAAI,IAAI,oBAAoB,MAAM;EAChC,OAAO,EAAE,KAAK,KAAK,QAAQ,KAAK;GAC9B,gBAAgB;GAChB,iBAAiB;GACjB,MAAM,IAAI,KAAK,KAAK;EACtB,CAAC;CACH,CAAC;AACH"}
1
+ {"version":3,"file":"assets.mjs","names":[],"sources":["../../src/routers/assets.ts"],"sourcesContent":["import type { Context } from \"hono\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { assetManifest } from \"../asset-manifest.ts\";\n\nconst IMMUTABLE = \"public, max-age=31536000, immutable\";\n\ninterface Asset {\n source: string;\n hash: string;\n contentType: string;\n}\n\n/**\n * Serve a content-addressed asset. A matching hash is cacheable forever and\n * answers conditional requests with 304; a stale hash (an old page still\n * open after an upgrade) gets the current content but is never cached under\n * the old URL.\n */\nfunction serveHashed(c: Context, asset: Asset, requestedHash: string): Response {\n const etag = `\"${asset.hash}\"`;\n if (requestedHash !== asset.hash) {\n return c.body(asset.source, 200, {\n \"content-type\": asset.contentType,\n \"cache-control\": \"no-store\",\n });\n }\n if (c.req.header(\"if-none-match\") === etag) {\n return c.body(null, 304, { etag, \"cache-control\": IMMUTABLE });\n }\n return c.body(asset.source, 200, {\n \"content-type\": asset.contentType,\n \"cache-control\": IMMUTABLE,\n etag,\n });\n}\n\n/** Register the static-asset routes (HTMX bundle, shell script, icon sprite and mark). */\nexport function registerAssets(app: ShelfRouter): void {\n const { client, htmx, icons, mark } = assetManifest;\n app.get(String.raw`/assets/:file{htmx-[a-f0-9]+\\.js}`, (c) => {\n const hash = (c.req.param(\"file\") ?? \"\").replaceAll(/^htmx-|\\.js$/gu, \"\");\n return serveHashed(c, { ...htmx, contentType: \"application/javascript\" }, hash);\n });\n app.get(String.raw`/assets/:file{client-[a-f0-9]+\\.js}`, (c) => {\n const hash = (c.req.param(\"file\") ?? \"\").replaceAll(/^client-|\\.js$/gu, \"\");\n return serveHashed(c, { ...client, contentType: \"application/javascript\" }, hash);\n });\n app.get(String.raw`/assets/:file{icons-[a-f0-9]+\\.svg}`, (c) => {\n const hash = (c.req.param(\"file\") ?? \"\").replaceAll(/^icons-|\\.svg$/gu, \"\");\n return serveHashed(c, { ...icons, contentType: \"image/svg+xml\" }, hash);\n });\n app.get(String.raw`/assets/:file{mark-[a-f0-9]+\\.svg}`, (c) => {\n const hash = (c.req.param(\"file\") ?? \"\").replaceAll(/^mark-|\\.svg$/gu, \"\");\n return serveHashed(c, { ...mark, contentType: \"image/svg+xml\" }, hash);\n });\n // Legacy unversioned URL: kept for old pages and external embeds, but\n // revalidated (never immutable) because its content can change.\n app.get(\"/assets/htmx.js\", (c) => {\n return c.body(htmx.source, 200, {\n \"content-type\": \"application/javascript\",\n \"cache-control\": \"no-cache\",\n etag: `\"${htmx.hash}\"`,\n });\n });\n}\n"],"mappings":";;;;;AAIA,MAAM,YAAY;;;;;;;AAclB,SAAS,YAAY,GAAY,OAAc,eAAiC;CAC9E,MAAM,OAAO,IAAI,MAAM,KAAK;CAC5B,IAAI,kBAAkB,MAAM,MAC1B,OAAO,EAAE,KAAK,MAAM,QAAQ,KAAK;EAC/B,gBAAgB,MAAM;EACtB,iBAAiB;CACnB,CAAC;CAEH,IAAI,EAAE,IAAI,OAAO,eAAe,MAAM,MACpC,OAAO,EAAE,KAAK,MAAM,KAAK;EAAE;EAAM,iBAAiB;CAAU,CAAC;CAE/D,OAAO,EAAE,KAAK,MAAM,QAAQ,KAAK;EAC/B,gBAAgB,MAAM;EACtB,iBAAiB;EACjB;CACF,CAAC;AACH;;AAGA,SAAgB,eAAe,KAAwB;CACrD,MAAM,EAAE,QAAQ,MAAM,OAAO,SAAS;CACtC,IAAI,IAAI,OAAO,GAAG,sCAAsC,MAAM;EAC5D,MAAM,QAAQ,EAAE,IAAI,MAAM,MAAM,KAAK,GAAA,CAAI,WAAW,kBAAkB,EAAE;EACxE,OAAO,YAAY,GAAG;GAAE,GAAG;GAAM,aAAa;EAAyB,GAAG,IAAI;CAChF,CAAC;CACD,IAAI,IAAI,OAAO,GAAG,wCAAwC,MAAM;EAC9D,MAAM,QAAQ,EAAE,IAAI,MAAM,MAAM,KAAK,GAAA,CAAI,WAAW,oBAAoB,EAAE;EAC1E,OAAO,YAAY,GAAG;GAAE,GAAG;GAAQ,aAAa;EAAyB,GAAG,IAAI;CAClF,CAAC;CACD,IAAI,IAAI,OAAO,GAAG,wCAAwC,MAAM;EAC9D,MAAM,QAAQ,EAAE,IAAI,MAAM,MAAM,KAAK,GAAA,CAAI,WAAW,oBAAoB,EAAE;EAC1E,OAAO,YAAY,GAAG;GAAE,GAAG;GAAO,aAAa;EAAgB,GAAG,IAAI;CACxE,CAAC;CACD,IAAI,IAAI,OAAO,GAAG,uCAAuC,MAAM;EAC7D,MAAM,QAAQ,EAAE,IAAI,MAAM,MAAM,KAAK,GAAA,CAAI,WAAW,mBAAmB,EAAE;EACzE,OAAO,YAAY,GAAG;GAAE,GAAG;GAAM,aAAa;EAAgB,GAAG,IAAI;CACvE,CAAC;CAGD,IAAI,IAAI,oBAAoB,MAAM;EAChC,OAAO,EAAE,KAAK,KAAK,QAAQ,KAAK;GAC9B,gBAAgB;GAChB,iBAAiB;GACjB,MAAM,IAAI,KAAK,KAAK;EACtB,CAAC;CACH,CAAC;AACH"}
@@ -7,7 +7,7 @@ import { attemptLogSchema, attemptSchema, notFound as notFound$1, unauthorized }
7
7
  import { VIEW_ROLES, buildForProject } from "./builds.handlers.mjs";
8
8
  import { createRoute, z } from "@hono/zod-openapi";
9
9
  import { CaptureAttemptModel, CaptureLogModel } from "@storyshelf/core/models";
10
- //#region src/routers/attempts.ts
10
+ // src/routers/attempts.ts
11
11
  /** Register the capture-attempt history and per-attempt log endpoints. */
12
12
  function registerAttempts(app) {
13
13
  app.openapi(listAttemptsRoute, async (c) => {
@@ -90,7 +90,6 @@ async function attemptForBuild(build, attemptNo) {
90
90
  if (!attempt) notFound("Attempt not found");
91
91
  return attempt;
92
92
  }
93
- //#endregion
94
93
  export { registerAttempts };
95
94
 
96
95
  //# sourceMappingURL=attempts.mjs.map
@@ -2,7 +2,7 @@ import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
4
  import { MemberModel, ProjectGroupMappingModel, ProjectModel, UserModel } from "@storyshelf/core/models";
5
- //#region src/routers/auth-sync.ts
5
+ // src/routers/auth-sync.ts
6
6
  const ROLE_RANK = {
7
7
  viewer: 0,
8
8
  developer: 1,
@@ -58,7 +58,6 @@ async function syncProjectMembership(members, mappings, projectId, userId, group
58
58
  const existing = await members.get(projectId, userId);
59
59
  if (!existing || existing.role !== top.role || existing.source !== `oidc:${top.groupName}`) await members.set(projectId, userId, top.role, `oidc:${top.groupName}`);
60
60
  }
61
- //#endregion
62
61
  export { syncLoginMemberships };
63
62
 
64
63
  //# sourceMappingURL=auth-sync.mjs.map
@@ -8,10 +8,10 @@ import { renderInviteInvalidPage, renderInvitePage } from "../pages/invite.mjs";
8
8
  import { renderLoginPage } from "../pages/login.mjs";
9
9
  import { syncLoginMemberships } from "./auth-sync.mjs";
10
10
  import { UserModel } from "@storyshelf/core/models";
11
- import { SESSION_COOKIE } from "@storyshelf/core/types";
12
11
  import { HTTPException } from "hono/http-exception";
12
+ import { SESSION_COOKIE } from "@storyshelf/core/types";
13
13
  import { MIN_PASSWORD_LENGTH } from "@storyshelf/core/auth";
14
- //#region src/routers/auth.ts
14
+ // src/routers/auth.ts
15
15
  /** Provider ids are URL path segments: strict charset, bounded length. */
16
16
  function validProviderId(value) {
17
17
  return /^[a-zA-Z0-9_-]{1,64}$/u.test(value);
@@ -232,7 +232,6 @@ function registerEngineAuth(app, engine) {
232
232
  return hxRedirect(c, "/auth/login");
233
233
  });
234
234
  }
235
- //#endregion
236
235
  export { forwardToEngine, registerEngineAuth };
237
236
 
238
237
  //# sourceMappingURL=auth.mjs.map
@@ -4,7 +4,7 @@ import.meta.url;
4
4
  import { getStore } from "../store.mjs";
5
5
  import { BaselineModel, baselineStatus, isBaselineDrifted } from "@storyshelf/core/models";
6
6
  import { HTTPException } from "hono/http-exception";
7
- //#region src/routers/baseline-guard.ts
7
+ // src/routers/baseline-guard.ts
8
8
  /** Refuse to promote a screenshot over a baseline the reviewer has not seen. */
9
9
  /** Whether a snapshot is awaiting a review decision. */
10
10
  function isOpenSnapshot(snapshot) {
@@ -12,7 +12,7 @@ function isOpenSnapshot(snapshot) {
12
12
  }
13
13
  /** Compare a snapshot's recorded baseline with the baseline that applies to it now. */
14
14
  async function snapshotBaselineStatus(project, build, snapshot) {
15
- const current = await new BaselineModel(getStore().db).resolve(project.id, snapshot.storyId, snapshot.viewportName, build.gitBranch, project.gitDefaultBranch);
15
+ const current = await new BaselineModel(getStore().db).resolve(project.id, snapshot.storyId, snapshot.viewportName, build.gitBranch, project.gitDefaultBranch, snapshot.browser);
16
16
  return baselineStatus(snapshot, current);
17
17
  }
18
18
  /** Open snapshots whose diff no longer matches the current baseline. */
@@ -63,7 +63,7 @@ async function loadBaselineView(project, build, snapshots) {
63
63
  const entries = await Promise.all(snapshots.map(async (snapshot) => {
64
64
  return {
65
65
  snapshot,
66
- current: await baselines.resolve(project.id, snapshot.storyId, snapshot.viewportName, build.gitBranch, project.gitDefaultBranch)
66
+ current: await baselines.resolve(project.id, snapshot.storyId, snapshot.viewportName, build.gitBranch, project.gitDefaultBranch, snapshot.browser)
67
67
  };
68
68
  }));
69
69
  const view = {
@@ -71,13 +71,12 @@ async function loadBaselineView(project, build, snapshots) {
71
71
  drifted: {}
72
72
  };
73
73
  for (const { snapshot, current } of entries) {
74
- view.hasBaseline[snapshot.id] = Boolean(current);
74
+ view.hasBaseline[snapshot.id] = Boolean(snapshot.comparisonImagePath ?? current);
75
75
  const status = baselineStatus(snapshot, current);
76
76
  if (!build.isDefault && isOpenSnapshot(snapshot) && (status === "stale" || status === "removed")) view.drifted[snapshot.id] = status;
77
77
  }
78
78
  return view;
79
79
  }
80
- //#endregion
81
80
  export { assertBaselineCurrent, baselineChanged, findDriftedSnapshots, isBaselineChanged, isOpenSnapshot, loadBaselineView, snapshotBaselineStatus };
82
81
 
83
82
  //# sourceMappingURL=baseline-guard.mjs.map