@storyshelf/app 0.7.0 → 0.9.0-next-d2cc082

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 (270) hide show
  1. package/dist/app-types.d.mts +1 -3
  2. package/dist/asset-manifest.mjs +1 -2
  3. package/dist/assets/htmx.mjs +1 -2
  4. package/dist/assets/mark.mjs +1 -2
  5. package/dist/boot-tasks.mjs +1 -2
  6. package/dist/build-state-flags.mjs +1 -2
  7. package/dist/build-state-setup.mjs +1 -2
  8. package/dist/capture-alerts.mjs +1 -2
  9. package/dist/capture-process.mjs +1 -2
  10. package/dist/capture-setup.mjs +1 -2
  11. package/dist/connections.mjs +1 -2
  12. package/dist/consumer-setup.mjs +1 -2
  13. package/dist/credential-keys.mjs +1 -2
  14. package/dist/credentials.mjs +1 -2
  15. package/dist/design-events.mjs +71 -0
  16. package/dist/design-events.mjs.map +1 -0
  17. package/dist/git-status.mjs +1 -2
  18. package/dist/hidden-data.mjs +1 -2
  19. package/dist/index.d.mts +0 -2
  20. package/dist/index.mjs +2 -3
  21. package/dist/insights/budget.mjs +1 -2
  22. package/dist/insights/deps.mjs +1 -2
  23. package/dist/insights/errors.mjs +1 -2
  24. package/dist/insights/health-panel.mjs +0 -1
  25. package/dist/insights/job.mjs +1 -2
  26. package/dist/insights/load-health.mjs +1 -2
  27. package/dist/insights/load-triage.mjs +1 -2
  28. package/dist/insights/panel.mjs +1 -2
  29. package/dist/insights/request.mjs +1 -2
  30. package/dist/insights/stale.mjs +1 -2
  31. package/dist/insights/usage-report.mjs +1 -2
  32. package/dist/insights/view.mjs +1 -2
  33. package/dist/job-canceller.mjs +1 -2
  34. package/dist/job-queue.mjs +1 -2
  35. package/dist/jobs-setup.mjs +1 -2
  36. package/dist/lifecycle.mjs +2 -3
  37. package/dist/lifecycle.mjs.map +1 -1
  38. package/dist/middleware/auth-gate.mjs +1 -2
  39. package/dist/middleware/csrf.mjs +1 -2
  40. package/dist/middleware/rate-limit.mjs +1 -2
  41. package/dist/middleware/request-log.mjs +1 -2
  42. package/dist/middleware/require-review.mjs +1 -2
  43. package/dist/middleware/require-testing.mjs +1 -2
  44. package/dist/middleware/server-timing.mjs +1 -2
  45. package/dist/middleware/setup-gate.mjs +1 -2
  46. package/dist/middleware/store-scope.mjs +1 -2
  47. package/dist/notifications-pause.mjs +1 -2
  48. package/dist/notify.d.mts +1 -3
  49. package/dist/notify.mjs +5 -3
  50. package/dist/notify.mjs.map +1 -1
  51. package/dist/pages/a11y-violations.mjs +1 -2
  52. package/dist/pages/admin-ai.mjs +1 -2
  53. package/dist/pages/admin-system-origin.mjs +19 -10
  54. package/dist/pages/admin-system-origin.mjs.map +1 -1
  55. package/dist/pages/admin-system-targets.mjs +1 -2
  56. package/dist/pages/admin-system.mjs +1 -2
  57. package/dist/pages/build-detail.mjs +1 -2
  58. package/dist/pages/build-diff-actions.mjs +1 -2
  59. package/dist/pages/build-diff-comments.mjs +1 -2
  60. package/dist/pages/build-diff-default.mjs +53 -0
  61. package/dist/pages/build-diff-default.mjs.map +1 -0
  62. package/dist/pages/build-diff-environment.mjs +34 -5
  63. package/dist/pages/build-diff-environment.mjs.map +1 -1
  64. package/dist/pages/build-diff-header.mjs +1 -2
  65. package/dist/pages/build-diff-insight.mjs +1 -2
  66. package/dist/pages/build-diff-model.mjs +20 -3
  67. package/dist/pages/build-diff-model.mjs.map +1 -1
  68. package/dist/pages/build-diff-nav.mjs +1 -2
  69. package/dist/pages/build-diff-reference.mjs +84 -0
  70. package/dist/pages/build-diff-reference.mjs.map +1 -0
  71. package/dist/pages/build-diff-shortcuts.mjs +1 -2
  72. package/dist/pages/build-diff-stale.mjs +1 -2
  73. package/dist/pages/build-diff-usage.mjs +1 -2
  74. package/dist/pages/build-diff-viewer.mjs +38 -12
  75. package/dist/pages/build-diff-viewer.mjs.map +1 -1
  76. package/dist/pages/build-diff.mjs +6 -12
  77. package/dist/pages/build-diff.mjs.map +1 -1
  78. package/dist/pages/build-tests.mjs +1 -2
  79. package/dist/pages/component-detail.mjs +1 -2
  80. package/dist/pages/component-stories.mjs +1 -2
  81. package/dist/pages/compute-jobs.mjs +1 -2
  82. package/dist/pages/design-links.mjs +1 -2
  83. package/dist/pages/environment-change.mjs +16 -4
  84. package/dist/pages/environment-change.mjs.map +1 -1
  85. package/dist/pages/health-panel.mjs +1 -2
  86. package/dist/pages/invite.mjs +1 -2
  87. package/dist/pages/label-detail.mjs +1 -2
  88. package/dist/pages/library-grid.mjs +1 -2
  89. package/dist/pages/library-model.mjs +1 -2
  90. package/dist/pages/library-snapshots.mjs +1 -2
  91. package/dist/pages/library.mjs +1 -2
  92. package/dist/pages/login.mjs +1 -2
  93. package/dist/pages/notifications-paused.mjs +1 -2
  94. package/dist/pages/passkey-ceremony.mjs +1 -2
  95. package/dist/pages/profile.mjs +1 -2
  96. package/dist/pages/project-builds-table.mjs +1 -2
  97. package/dist/pages/project-builds.mjs +1 -2
  98. package/dist/pages/project-create.mjs +1 -2
  99. package/dist/pages/project-settings.mjs +1 -2
  100. package/dist/pages/projects-card.mjs +1 -2
  101. package/dist/pages/projects.mjs +1 -2
  102. package/dist/pages/review-changeset.mjs +1 -2
  103. package/dist/pages/review-comments.mjs +1 -2
  104. package/dist/pages/review-verdict.mjs +1 -2
  105. package/dist/pages/root.mjs +1 -2
  106. package/dist/pages/settings-ai.mjs +1 -2
  107. package/dist/pages/settings-capture.mjs +1 -2
  108. package/dist/pages/settings-design-credential.mjs +1 -2
  109. package/dist/pages/settings-design-links.mjs +1 -2
  110. package/dist/pages/settings-design.mjs +1 -2
  111. package/dist/pages/settings-general.mjs +1 -2
  112. package/dist/pages/settings-labels.mjs +1 -2
  113. package/dist/pages/settings-links.mjs +1 -2
  114. package/dist/pages/settings-members.mjs +1 -2
  115. package/dist/pages/settings-notifications-git.mjs +1 -2
  116. package/dist/pages/settings-notifications.mjs +1 -2
  117. package/dist/pages/settings-tests.mjs +1 -2
  118. package/dist/pages/settings-tokens.mjs +1 -2
  119. package/dist/pages/settings-webhooks.mjs +1 -2
  120. package/dist/pages/storybook.mjs +1 -2
  121. package/dist/pages/target-choices.mjs +1 -2
  122. package/dist/publish-setup.mjs +1 -2
  123. package/dist/retention-timer.mjs +10 -11
  124. package/dist/retention-timer.mjs.map +1 -1
  125. package/dist/review-events.mjs +1 -2
  126. package/dist/routers/admin-pages.mjs +1 -2
  127. package/dist/routers/admin-public-origin.mjs +10 -3
  128. package/dist/routers/admin-public-origin.mjs.map +1 -1
  129. package/dist/routers/admin.mjs +8 -4
  130. package/dist/routers/admin.mjs.map +1 -1
  131. package/dist/routers/assets.mjs +1 -2
  132. package/dist/routers/attempts.mjs +1 -2
  133. package/dist/routers/auth-sync.mjs +1 -2
  134. package/dist/routers/auth.mjs +1 -2
  135. package/dist/routers/baseline-guard.mjs +2 -3
  136. package/dist/routers/baseline-guard.mjs.map +1 -1
  137. package/dist/routers/baseline-reference.mjs +72 -0
  138. package/dist/routers/baseline-reference.mjs.map +1 -0
  139. package/dist/routers/baselines.handlers.mjs +1 -2
  140. package/dist/routers/baselines.mjs +1 -2
  141. package/dist/routers/build-cancel.mjs +1 -2
  142. package/dist/routers/build-changeset.mjs +1 -2
  143. package/dist/routers/build-reviews.handlers.mjs +1 -2
  144. package/dist/routers/build-reviews.mjs +1 -2
  145. package/dist/routers/builds.handlers.mjs +23 -9
  146. package/dist/routers/builds.handlers.mjs.map +1 -1
  147. package/dist/routers/builds.mjs +7 -3
  148. package/dist/routers/builds.mjs.map +1 -1
  149. package/dist/routers/capture-cache.mjs +29 -0
  150. package/dist/routers/capture-cache.mjs.map +1 -0
  151. package/dist/routers/capture-config.mjs +1 -2
  152. package/dist/routers/comments.mjs +1 -2
  153. package/dist/routers/comparison-image.mjs +29 -0
  154. package/dist/routers/comparison-image.mjs.map +1 -0
  155. package/dist/routers/component-catalog.handlers.mjs +1 -2
  156. package/dist/routers/component.handlers.mjs +1 -2
  157. package/dist/routers/connections.mjs +1 -2
  158. package/dist/routers/design-connect.handlers.mjs +1 -2
  159. package/dist/routers/design.handlers.mjs +8 -2
  160. package/dist/routers/design.handlers.mjs.map +1 -1
  161. package/dist/routers/flash.mjs +1 -2
  162. package/dist/routers/health-report.mjs +1 -2
  163. package/dist/routers/health.mjs +1 -2
  164. package/dist/routers/helpers.mjs +1 -2
  165. package/dist/routers/htmx.mjs +1 -2
  166. package/dist/routers/insights-ui.mjs +1 -2
  167. package/dist/routers/insights.handlers.mjs +1 -2
  168. package/dist/routers/insights.mjs +1 -2
  169. package/dist/routers/labels.mjs +1 -2
  170. package/dist/routers/links.handlers.mjs +1 -2
  171. package/dist/routers/links.mjs +1 -2
  172. package/dist/routers/media.mjs +14 -5
  173. package/dist/routers/media.mjs.map +1 -1
  174. package/dist/routers/members.mjs +1 -2
  175. package/dist/routers/notification-channels.mjs +1 -2
  176. package/dist/routers/notification-subscriptions.mjs +1 -2
  177. package/dist/routers/oauth-state.mjs +1 -2
  178. package/dist/routers/preview-base.mjs +1 -2
  179. package/dist/routers/preview-token.mjs +1 -2
  180. package/dist/routers/profile.mjs +1 -2
  181. package/dist/routers/projects.mjs +1 -2
  182. package/dist/routers/publish-upload.mjs +19 -12
  183. package/dist/routers/publish-upload.mjs.map +1 -1
  184. package/dist/routers/published-access.mjs +1 -2
  185. package/dist/routers/published-compression.mjs +109 -0
  186. package/dist/routers/published-compression.mjs.map +1 -0
  187. package/dist/routers/published-files.mjs +26 -9
  188. package/dist/routers/published-files.mjs.map +1 -1
  189. package/dist/routers/review-forms.mjs +1 -2
  190. package/dist/routers/review-next.mjs +1 -2
  191. package/dist/routers/review-section.handlers.mjs +1 -2
  192. package/dist/routers/review-switch-off.mjs +1 -2
  193. package/dist/routers/schemas.mjs +1 -2
  194. package/dist/routers/scoped-models.mjs +1 -2
  195. package/dist/routers/settings-ai.mjs +1 -2
  196. package/dist/routers/settings-capture.mjs +1 -2
  197. package/dist/routers/settings-design.mjs +1 -2
  198. package/dist/routers/settings-general.mjs +1 -2
  199. package/dist/routers/settings-git-channel.mjs +1 -2
  200. package/dist/routers/settings-labels.mjs +1 -2
  201. package/dist/routers/settings-links.mjs +1 -2
  202. package/dist/routers/settings-members.mjs +1 -2
  203. package/dist/routers/settings-notifications.mjs +1 -2
  204. package/dist/routers/settings-tokens.mjs +1 -2
  205. package/dist/routers/settings-webhooks.mjs +1 -2
  206. package/dist/routers/settings.handlers.mjs +1 -2
  207. package/dist/routers/settings.mjs +1 -2
  208. package/dist/routers/signed-storybook.mjs +5 -3
  209. package/dist/routers/signed-storybook.mjs.map +1 -1
  210. package/dist/routers/snapshot-groups.mjs +1 -2
  211. package/dist/routers/snapshots.mjs +1 -2
  212. package/dist/routers/storybook.mjs +21 -17
  213. package/dist/routers/storybook.mjs.map +1 -1
  214. package/dist/routers/tokens.mjs +1 -2
  215. package/dist/routers/ui.mjs +4 -2
  216. package/dist/routers/ui.mjs.map +1 -1
  217. package/dist/routers/webhooks.mjs +1 -2
  218. package/dist/routers/well-known.mjs +1 -2
  219. package/dist/routes/api-routes.mjs +1 -2
  220. package/dist/routes/classification.mjs +1 -2
  221. package/dist/routes/openapi-classes.mjs +1 -2
  222. package/dist/routes/page-routes.mjs +2 -3
  223. package/dist/routes/page-routes.mjs.map +1 -1
  224. package/dist/runtime.mjs +1 -2
  225. package/dist/serverless.mjs +1 -2
  226. package/dist/store.mjs +1 -2
  227. package/dist/targets.mjs +1 -2
  228. package/dist/ui/avatar.mjs +1 -2
  229. package/dist/ui/buttons.mjs +1 -2
  230. package/dist/ui/code-block.mjs +1 -2
  231. package/dist/ui/compare-stage.mjs +86 -54
  232. package/dist/ui/compare-stage.mjs.map +1 -1
  233. package/dist/ui/csrf-field.mjs +1 -2
  234. package/dist/ui/css.mjs +1 -2
  235. package/dist/ui/design-reference.mjs +1 -2
  236. package/dist/ui/document.mjs +1 -2
  237. package/dist/ui/dropdown.mjs +1 -2
  238. package/dist/ui/feedback.mjs +1 -2
  239. package/dist/ui/filter-input.mjs +1 -2
  240. package/dist/ui/forms.mjs +1 -2
  241. package/dist/ui/icons/icon.mjs +1 -2
  242. package/dist/ui/icons/paths.mjs +1 -2
  243. package/dist/ui/icons/sprite.mjs +1 -2
  244. package/dist/ui/kbd.mjs +1 -2
  245. package/dist/ui/layout.mjs +1 -2
  246. package/dist/ui/preview-embed.mjs +1 -2
  247. package/dist/ui/progress.mjs +1 -2
  248. package/dist/ui/segmented.mjs +1 -2
  249. package/dist/ui/shell/account.mjs +1 -2
  250. package/dist/ui/shell/bare.mjs +1 -2
  251. package/dist/ui/shell/boot-script.mjs +1 -2
  252. package/dist/ui/shell/client-script.mjs +1 -2
  253. package/dist/ui/shell/nav.mjs +1 -2
  254. package/dist/ui/shell/review-script.mjs +17 -3
  255. package/dist/ui/shell/review-script.mjs.map +1 -1
  256. package/dist/ui/shell/sidebar.mjs +1 -2
  257. package/dist/ui/shell/topbar.mjs +1 -2
  258. package/dist/ui/stacks.mjs +1 -2
  259. package/dist/ui/styles/base.mjs +1 -2
  260. package/dist/ui/styles/behavior.mjs +1 -2
  261. package/dist/ui/styles/review-layout.mjs +1 -2
  262. package/dist/ui/styles/review.mjs +1 -2
  263. package/dist/ui/styles/tokens.mjs +1 -2
  264. package/dist/ui/styles.mjs +1 -2
  265. package/dist/ui/subnav.mjs +1 -2
  266. package/dist/ui/table.mjs +1 -2
  267. package/dist/ui/theme.mjs +1 -2
  268. package/dist/ui/thumbnail.mjs +1 -2
  269. package/dist/ui/time.mjs +1 -2
  270. package/package.json +9 -18
@@ -7,7 +7,7 @@ import { FormActions, SelectField } from "../ui/forms.mjs";
7
7
  import { Card, SectionTitle } from "../ui/layout.mjs";
8
8
  import { csrfField } from "../ui/csrf-field.mjs";
9
9
  import { jsx, jsxs } from "hono/jsx/jsx-runtime";
10
- //#region src/pages/settings-ai.tsx
10
+ // src/pages/settings-ai.tsx
11
11
  function ProfileForm(props) {
12
12
  const { project, ai } = props;
13
13
  return /* @__PURE__ */ jsxs("form", {
@@ -63,7 +63,6 @@ function renderSettingsAi(project, ai, formState) {
63
63
  ] })]
64
64
  });
65
65
  }
66
- //#endregion
67
66
  export { renderSettingsAi };
68
67
 
69
68
  //# sourceMappingURL=settings-ai.mjs.map
@@ -11,7 +11,7 @@ import { MAX_STORYBOOK_URL_TTL, MIN_STORYBOOK_URL_TTL_OVERRIDE } from "@storyshe
11
11
  import { MAX_BROWSERS, parseBrowsers, parseModesAllowlist } from "@storyshelf/core/capture";
12
12
  import { Fragment, jsx, jsxs } from "hono/jsx/jsx-runtime";
13
13
  import { isCaptureTarget } from "@storyshelf/core/adapter/capture-runner";
14
- //#region src/pages/settings-capture.tsx
14
+ // src/pages/settings-capture.tsx
15
15
  /** Primary engine select plus one checkbox per additional engine. */
16
16
  function BrowserFields(props) {
17
17
  const { primary, extras, disabled, targets } = props;
@@ -118,7 +118,6 @@ function renderSettingsCapture(project, isAdmin, formState, offered) {
118
118
  ] })]
119
119
  });
120
120
  }
121
- //#endregion
122
121
  export { renderSettingsCapture };
123
122
 
124
123
  //# sourceMappingURL=settings-capture.mjs.map
@@ -8,7 +8,7 @@ import { Card, SectionTitle } from "../ui/layout.mjs";
8
8
  import { csrfField } from "../ui/csrf-field.mjs";
9
9
  import { RemoveForm } from "./settings-design-links.mjs";
10
10
  import { Fragment, jsx, jsxs } from "hono/jsx/jsx-runtime";
11
- //#region src/pages/settings-design-credential.tsx
11
+ // src/pages/settings-design-credential.tsx
12
12
  const STATUS_BADGES = {
13
13
  unknown: {
14
14
  tone: "neutral",
@@ -93,7 +93,6 @@ function CredentialForm(props) {
93
93
  ]
94
94
  });
95
95
  }
96
- //#endregion
97
96
  export { CredentialCard };
98
97
 
99
98
  //# sourceMappingURL=settings-design-credential.mjs.map
@@ -8,7 +8,7 @@ import { Card, SectionTitle } from "../ui/layout.mjs";
8
8
  import { Table } from "../ui/table.mjs";
9
9
  import { csrfField } from "../ui/csrf-field.mjs";
10
10
  import { jsx, jsxs } from "hono/jsx/jsx-runtime";
11
- //#region src/pages/settings-design-links.tsx
11
+ // src/pages/settings-design-links.tsx
12
12
  function FilesCard(props) {
13
13
  const { base, files, isAdmin, formState, urlHint } = props;
14
14
  return /* @__PURE__ */ jsxs(Card, { children: [
@@ -173,7 +173,6 @@ function RemoveForm(props) {
173
173
  })]
174
174
  });
175
175
  }
176
- //#endregion
177
176
  export { ComponentsCard, FilesCard, RemoveForm };
178
177
 
179
178
  //# sourceMappingURL=settings-design-links.mjs.map
@@ -5,7 +5,7 @@ import { Alert, EmptyState } from "../ui/feedback.mjs";
5
5
  import { ComponentsCard, FilesCard } from "./settings-design-links.mjs";
6
6
  import { CredentialCard } from "./settings-design-credential.mjs";
7
7
  import { jsx, jsxs } from "hono/jsx/jsx-runtime";
8
- //#region src/pages/settings-design.tsx
8
+ // src/pages/settings-design.tsx
9
9
  /** Design settings tab: project file links, component mapping and provider credentials (admin-only parts gated). */
10
10
  function renderSettingsDesign(project, design, isAdmin, formState) {
11
11
  const base = `/projects/${project.slug}/settings/design`;
@@ -42,7 +42,6 @@ function renderSettingsDesign(project, design, isAdmin, formState) {
42
42
  ]
43
43
  });
44
44
  }
45
- //#endregion
46
45
  export { renderSettingsDesign };
47
46
 
48
47
  //# sourceMappingURL=settings-design.mjs.map
@@ -9,7 +9,7 @@ import { csrfField } from "../ui/csrf-field.mjs";
9
9
  import { targetChoices } from "./target-choices.mjs";
10
10
  import { jsx, jsxs } from "hono/jsx/jsx-runtime";
11
11
  import { isCaptureTarget } from "@storyshelf/core/adapter/capture-runner";
12
- //#region src/pages/settings-general.tsx
12
+ // src/pages/settings-general.tsx
13
13
  /** General settings tab: project metadata, diff thresholds, and danger zone. */
14
14
  function renderSettingsGeneral(project, formState, isAdmin, offered) {
15
15
  const errors = formState?.errors ?? {};
@@ -157,7 +157,6 @@ function renderSettingsGeneral(project, formState, isAdmin, offered) {
157
157
  ]
158
158
  });
159
159
  }
160
- //#endregion
161
160
  export { renderSettingsGeneral };
162
161
 
163
162
  //# sourceMappingURL=settings-general.mjs.map
@@ -8,7 +8,7 @@ import { Card, SectionTitle } from "../ui/layout.mjs";
8
8
  import { Table } from "../ui/table.mjs";
9
9
  import { csrfField } from "../ui/csrf-field.mjs";
10
10
  import { jsx, jsxs } from "hono/jsx/jsx-runtime";
11
- //#region src/pages/settings-labels.tsx
11
+ // src/pages/settings-labels.tsx
12
12
  /** Labels settings tab: label-type table plus the create-type form. */
13
13
  function renderSettingsLabels(project, labelTypes, isAdmin, formState) {
14
14
  const editing = isAdmin ? labelTypes.find((item) => item.key === formState?.editKey) : void 0;
@@ -142,7 +142,6 @@ function renderSettingsLabels(project, labelTypes, isAdmin, formState) {
142
142
  ]
143
143
  });
144
144
  }
145
- //#endregion
146
145
  export { renderSettingsLabels };
147
146
 
148
147
  //# sourceMappingURL=settings-labels.mjs.map
@@ -8,7 +8,7 @@ import { Card, SectionTitle } from "../ui/layout.mjs";
8
8
  import { Table } from "../ui/table.mjs";
9
9
  import { csrfField } from "../ui/csrf-field.mjs";
10
10
  import { jsx, jsxs } from "hono/jsx/jsx-runtime";
11
- //#region src/pages/settings-links.tsx
11
+ // src/pages/settings-links.tsx
12
12
  const linkRow = (base, link, isAdmin) => /* @__PURE__ */ jsxs("tr", { children: [
13
13
  /* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx(Badge, {
14
14
  tone: "neutral",
@@ -106,7 +106,6 @@ function AddLinkCard(props) {
106
106
  ]
107
107
  })] });
108
108
  }
109
- //#endregion
110
109
  export { renderSettingsLinks };
111
110
 
112
111
  //# sourceMappingURL=settings-links.mjs.map
@@ -8,7 +8,7 @@ import { Card, SectionTitle } from "../ui/layout.mjs";
8
8
  import { Table } from "../ui/table.mjs";
9
9
  import { csrfField } from "../ui/csrf-field.mjs";
10
10
  import { jsx, jsxs } from "hono/jsx/jsx-runtime";
11
- //#region src/pages/settings-members.tsx
11
+ // src/pages/settings-members.tsx
12
12
  /** Members settings tab: member roster plus the add-member form. */
13
13
  function renderSettingsMembers(project, members, groupMappings, isAdmin, formState) {
14
14
  return /* @__PURE__ */ jsxs("div", {
@@ -186,7 +186,6 @@ function renderSettingsMembers(project, members, groupMappings, isAdmin, formSta
186
186
  ]
187
187
  });
188
188
  }
189
- //#endregion
190
189
  export { renderSettingsMembers };
191
190
 
192
191
  //# sourceMappingURL=settings-members.mjs.map
@@ -8,7 +8,7 @@ import { Card, SectionTitle } from "../ui/layout.mjs";
8
8
  import { csrfField } from "../ui/csrf-field.mjs";
9
9
  import { isServerlessHost } from "@storyshelf/core/config";
10
10
  import { Fragment, jsx, jsxs } from "hono/jsx/jsx-runtime";
11
- //#region src/pages/settings-notifications-git.tsx
11
+ // src/pages/settings-notifications-git.tsx
12
12
  /** GitHub only: show builds as check runs posted by a GitHub App, not as commit statuses. */
13
13
  const GitHubAppFields = ({ errors }) => /* @__PURE__ */ jsxs(Fragment, { children: [
14
14
  /* @__PURE__ */ jsx(SelectField, {
@@ -122,7 +122,6 @@ const CreateGitChannelCard = ({ project, providers, formState }) => /* @__PURE__
122
122
  ]
123
123
  })
124
124
  ] });
125
- //#endregion
126
125
  export { CreateGitChannelCard };
127
126
 
128
127
  //# sourceMappingURL=settings-notifications-git.mjs.map
@@ -12,7 +12,7 @@ import { csrfField } from "../ui/csrf-field.mjs";
12
12
  import { NotificationsPausedNotice } from "./notifications-paused.mjs";
13
13
  import { CreateGitChannelCard } from "./settings-notifications-git.mjs";
14
14
  import { jsx, jsxs } from "hono/jsx/jsx-runtime";
15
- //#region src/pages/settings-notifications.tsx
15
+ // src/pages/settings-notifications.tsx
16
16
  /** Notification topic catalog (mirrors the webhook event names). */
17
17
  const NOTIFICATION_TOPICS = [
18
18
  {
@@ -271,7 +271,6 @@ function renderSettingsNotifications(project, channels, providers, mySubscriptio
271
271
  ]
272
272
  });
273
273
  }
274
- //#endregion
275
274
  export { NOTIFICATION_TOPICS, renderSettingsNotifications };
276
275
 
277
276
  //# sourceMappingURL=settings-notifications.mjs.map
@@ -7,7 +7,7 @@ import { CheckField, Field, FormActions } from "../ui/forms.mjs";
7
7
  import { Card, SectionTitle } from "../ui/layout.mjs";
8
8
  import { csrfField } from "../ui/csrf-field.mjs";
9
9
  import { jsx, jsxs } from "hono/jsx/jsx-runtime";
10
- //#region src/pages/settings-tests.tsx
10
+ // src/pages/settings-tests.tsx
11
11
  /** Tests settings tab: play execution, Storybook 10 test runs, accessibility checks, play timeout. */
12
12
  function renderSettingsTests(project, isAdmin, formState) {
13
13
  return /* @__PURE__ */ jsxs("div", {
@@ -136,7 +136,6 @@ export const Flaky2: Story = {
136
136
  ]
137
137
  });
138
138
  }
139
- //#endregion
140
139
  export { renderSettingsTests };
141
140
 
142
141
  //# sourceMappingURL=settings-tests.mjs.map
@@ -8,7 +8,7 @@ import { Card, SectionTitle } from "../ui/layout.mjs";
8
8
  import { Table } from "../ui/table.mjs";
9
9
  import { csrfField } from "../ui/csrf-field.mjs";
10
10
  import { jsx, jsxs } from "hono/jsx/jsx-runtime";
11
- //#region src/pages/settings-tokens.tsx
11
+ // src/pages/settings-tokens.tsx
12
12
  /** Tokens settings tab: project CLI tokens plus the create-token form. */
13
13
  function renderSettingsTokens(project, tokens, isAdmin, formState) {
14
14
  return /* @__PURE__ */ jsxs("div", {
@@ -84,7 +84,6 @@ function renderSettingsTokens(project, tokens, isAdmin, formState) {
84
84
  ]
85
85
  });
86
86
  }
87
- //#endregion
88
87
  export { renderSettingsTokens };
89
88
 
90
89
  //# sourceMappingURL=settings-tokens.mjs.map
@@ -9,7 +9,7 @@ import { HStack } from "../ui/stacks.mjs";
9
9
  import { Table } from "../ui/table.mjs";
10
10
  import { csrfField } from "../ui/csrf-field.mjs";
11
11
  import { jsx, jsxs } from "hono/jsx/jsx-runtime";
12
- //#region src/pages/settings-webhooks.tsx
12
+ // src/pages/settings-webhooks.tsx
13
13
  /** Webhooks settings tab: webhook list plus the create-webhook form. */
14
14
  function renderSettingsWebhooks(project, webhooks, isAdmin, formState) {
15
15
  return /* @__PURE__ */ jsxs("div", {
@@ -98,7 +98,6 @@ function renderSettingsWebhooks(project, webhooks, isAdmin, formState) {
98
98
  ]
99
99
  });
100
100
  }
101
- //#endregion
102
101
  export { renderSettingsWebhooks };
103
102
 
104
103
  //# sourceMappingURL=settings-webhooks.mjs.map
@@ -2,7 +2,7 @@ import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
4
  import { getStore } from "../store.mjs";
5
- //#region src/pages/storybook.tsx
5
+ // src/pages/storybook.tsx
6
6
  /**
7
7
  * Placeholder shown while a build's statics are not yet available.
8
8
  * Refreshes itself; once extraction lands, the route serves the live page.
@@ -110,7 +110,6 @@ function escapeHtml(value) {
110
110
  function escapeAttr(value) {
111
111
  return escapeHtml(value);
112
112
  }
113
- //#endregion
114
113
  export { renderStorybookPage, renderStorybookPreparingPage };
115
114
 
116
115
  //# sourceMappingURL=storybook.mjs.map
@@ -3,7 +3,7 @@ import "node:url";
3
3
  import.meta.url;
4
4
  import { targetLabel } from "../targets.mjs";
5
5
  import { BROWSER_NAMES, unavailableReason } from "@storyshelf/core/adapter/capture-runner";
6
- //#region src/pages/target-choices.ts
6
+ // src/pages/target-choices.ts
7
7
  /** The capture targets a project can pick, from what the runner offers (ADR 0007, decision 7). */
8
8
  /**
9
9
  * Selectable targets: every ready one; a project's current target that is no longer ready stays,
@@ -33,7 +33,6 @@ function targetChoices(offered, current) {
33
33
  })
34
34
  };
35
35
  }
36
- //#endregion
37
36
  export { targetChoices };
38
37
 
39
38
  //# sourceMappingURL=target-choices.mjs.map
@@ -3,7 +3,7 @@ import "node:url";
3
3
  import.meta.url;
4
4
  import { DEFAULT_SCRATCH_DIR } from "@storyshelf/core/config";
5
5
  import { createCatalogHook, createCatalogLoader, createPublishCompleter, createPublishHandler, extractionLimitsOf, resolvePublishLimits } from "@storyshelf/core/capture";
6
- //#region src/publish-setup.ts
6
+ // src/publish-setup.ts
7
7
  /**
8
8
  * Publishing wiring (ADR 0006, decision 10): the completer every upload route ends in, and the
9
9
  * `publish` job kind on the shared database queue. Its handler runs on every instance with a
@@ -83,7 +83,6 @@ function publishHooks(catalog, captureWillRun, delivery) {
83
83
  }
84
84
  ];
85
85
  }
86
- //#endregion
87
86
  export { setupPublishing };
88
87
 
89
88
  //# sourceMappingURL=publish-setup.mjs.map
@@ -3,7 +3,7 @@ import "node:url";
3
3
  import.meta.url;
4
4
  import { ProjectModel } from "@storyshelf/core/models";
5
5
  import { Retention, purgeAiData } from "@storyshelf/core/retention";
6
- //#region src/retention-timer.ts
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,
@@ -47,20 +51,15 @@ function createBranchGcRunner(db, storage, logger, ttlDays) {
47
51
  };
48
52
  }
49
53
  function scheduleBranchGc(run, intervalMs) {
50
- const timeout = setTimeout(() => {
51
- run();
52
- }, Math.min(36e5, intervalMs));
54
+ const timeout = setTimeout(() => run(), Math.min(36e5, intervalMs));
53
55
  timeout.unref?.();
54
- const interval = setInterval(() => {
55
- run();
56
- }, intervalMs);
56
+ const interval = setInterval(() => run(), intervalMs);
57
57
  interval.unref?.();
58
58
  return { stop: () => {
59
59
  clearTimeout(timeout);
60
60
  clearInterval(interval);
61
61
  } };
62
62
  }
63
- //#endregion
64
63
  export { startBranchGcTimer };
65
64
 
66
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, 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): 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 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 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,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;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"}
@@ -5,7 +5,7 @@ import { credentialKeys } from "./credential-keys.mjs";
5
5
  import { getStore } from "./store.mjs";
6
6
  import { notifyProjectWith } from "./notify.mjs";
7
7
  import { emitWebhookEvent } from "@storyshelf/core/adapter/webhook-events";
8
- //#region src/review-events.ts
8
+ // src/review-events.ts
9
9
  /**
10
10
  * Review events (ADR 0013, decisions 4 and 10; ADR 0014, decision 10). They ride the existing
11
11
  * paths: signed webhooks to the project's subscribers and the notification fan-out (stored
@@ -42,7 +42,6 @@ function reviewRequestedData(build) {
42
42
  message: build.message
43
43
  };
44
44
  }
45
- //#endregion
46
45
  export { REVIEW_EVENTS, announceReview, announceReviewWith, reviewRequestedData };
47
46
 
48
47
  //# sourceMappingURL=review-events.mjs.map
@@ -15,7 +15,7 @@ import { hxRedirect } from "./htmx.mjs";
15
15
  import { flash } from "./flash.mjs";
16
16
  import { collectHealthReport } from "./health-report.mjs";
17
17
  import { describeCaptureHost } from "@storyshelf/core/utils";
18
- //#region src/routers/admin-pages.ts
18
+ // src/routers/admin-pages.ts
19
19
  /** Register the site-admin System page (adapter inventory + in-depth health). */
20
20
  function registerAdminPages(app, deps, auth) {
21
21
  app.get("/admin", async (c) => {
@@ -84,7 +84,6 @@ async function publicOriginView() {
84
84
  learns: authEnabled
85
85
  };
86
86
  }
87
- //#endregion
88
87
  export { registerAdminPages };
89
88
 
90
89
  //# sourceMappingURL=admin-pages.mjs.map
@@ -8,7 +8,7 @@ import { flash } from "./flash.mjs";
8
8
  import { requestOpenBuildStatesNow } from "../git-status.mjs";
9
9
  import { normalizeOrigin } from "@storyshelf/core/utils";
10
10
  import { HTTPException } from "hono/http-exception";
11
- //#region src/routers/admin-public-origin.ts
11
+ // src/routers/admin-public-origin.ts
12
12
  /**
13
13
  * The public origin, learnt from an admin's browser (ADR 0007, decision 6): the System page reports
14
14
  * `location.origin`, an admin confirms or replaces it, and background jobs read it from the database.
@@ -20,6 +20,13 @@ function registerPublicOriginRoutes(app) {
20
20
  app.post("/admin/public-origin", handleSet);
21
21
  app.post("/admin/public-origin/clear", handleClear);
22
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
+ }
23
30
  /** The origin a report names, once it matches what the browser says it used; throws 400 otherwise. */
24
31
  async function reportedOrigin(c) {
25
32
  const body = await c.req.json().catch(() => null);
@@ -31,7 +38,7 @@ async function reportedOrigin(c) {
31
38
  async function handleReport(c) {
32
39
  requireSiteAdmin(c);
33
40
  const { authEnabled, publicOrigin, user } = getStore();
34
- if (!authEnabled || publicOrigin === void 0) return c.body(null, 204);
41
+ if (!authEnabled || publicOrigin === void 0 || await isFixed()) return c.body(null, 204);
35
42
  const reported = await reportedOrigin(c);
36
43
  const before = await publicOrigin.stored();
37
44
  const wasResolved = await publicOrigin.url() !== void 0;
@@ -48,6 +55,7 @@ async function resumeIfResolved(wasResolved) {
48
55
  }
49
56
  /** Store what an admin typed; the message says why not when it is no origin. */
50
57
  async function storeTyped(c) {
58
+ if (await isFixed()) return FIXED_ORIGIN_MESSAGE;
51
59
  const { publicOrigin, user } = getStore();
52
60
  const origin = (await c.req.formData()).get("origin");
53
61
  try {
@@ -79,7 +87,6 @@ async function handleClear(c) {
79
87
  flash(c, "Stored public origin removed");
80
88
  return hxRedirect(c, "/admin");
81
89
  }
82
- //#endregion
83
90
  export { registerPublicOriginRoutes };
84
91
 
85
92
  //# sourceMappingURL=admin-public-origin.mjs.map
@@ -1 +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/** 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) {\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 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,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,GACnC,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,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"}
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":";;;;;;;;;;;;AAUA,MAAM,aAAa,YAAY;CAC7B,QAAQ;CACR,MAAM,CAAC,OAAO;CACd,SAAS;CACT,MAAM;CACN,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,oBAAoB,EAAE,QAAQ,iBAAiB,EAAE,EAAE,EAAE;CACnF,WAAW;EACT,KAAK;GACH,SAAS,EAAE,oBAAoB,EAAE,QAAQ,YAAY,EAAE;GACvD,aAAa;EACf;EACA,GAAGA;CACL;AACF,CAAC;AAED,MAAM,kBAAkB,EAAE,OAAO;CAC/B,aAAa,EAAE,OAAO,CAAC,CAAC,IAAI;CAC5B,QAAQ,EAAE,OAAO,CAAC,CAAC,IAAI;CACvB,YAAY,EAAE,OAAO,CAAC,CAAC,IAAI;AAC7B,CAAC;AAED,MAAM,iBAAiB,YAAY;CACjC,QAAQ;CACR,MAAM;CACN,MAAM,CAAC,OAAO;CACd,SAAS;CACT,WAAW;EACT,KAAK;GACH,SAAS,EAAE,oBAAoB,EAAE,QAAQ,gBAAgB,EAAE;GAC3D,aAAa;EACf;EACA,KAAK;GACH,SAAS,EAAE,oBAAoB,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,EAAE;GAC3E,aAAa;EACf;EACA,GAAGA;CACL;AACF,CAAC;;AAGD,SAAgB,cAAc,KAAwB;CACpD,IAAI,QAAQ,gBAAgB,OAAO,MAAM;EACvC,iBAAiB,CAAC;EAClB,MAAM,EAAE,IAAI,WAAW,SAAS;EAChC,IAAI,CAAC,OAAO,gBACV,OAAO,EAAE,KAAK,EAAE,OAAO,yDAAyD,GAAG,GAAG;EAExF,MAAM,SAAS,MAAM,qBAAqB,IAAI,MAAM;EACpD,OAAO,EAAE,KACP;GACE,aAAa,OAAO;GACpB,QAAQ,OAAO;GACf,YAAY,OAAO,WAAW;EAChC,GACA,GACF;CACF,CAAC;CAED,IAAI,QAAQ,YAAY,OAAO,MAAM;EACnC,iBAAiB,CAAC;EAElB,MAAM,UADO,EAAE,IAAI,MAAM,MACN,CAAC,CAAC,WAAW,SAAS,CAAC,CAAC,OAAO,gBAAgB;EAClE,MAAM,gBAAgB,MAAM,YAAY,OAAO;EAC/C,MAAM,SAAS,MAAM,cAAc;EACnC,MAAM,aAAa,uBAAuB;GACxC;GACA,iBAAiB,OAAO;GACxB,kBAAkB,OAAO;GACzB;EACF,CAAC;EACD,OAAO,EAAE,KAAK;GACZ;GACA,iBAAiB,OAAO;GACxB,kBAAkB,OAAO;EAC3B,CAAC;CACH,CAAC;AACH;AAGA,eAAe,YAAY,SAAkC;CAC3D,MAAM,WAAW,MAAM,IAAI,aAAa,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK;CAC5D,MAAM,YAAY,IAAI,UACpB,SAAS,CAAC,CAAC,IACX,SAAS,CAAC,CAAC,SACX;EACE,QAAQ,SAAS,CAAC,CAAC,GAAG,OAAO;EAC7B,aAAa,SAAS,CAAC,CAAC,GAAG,OAAO;EAClC,WAAW,SAAS,CAAC,CAAC,GAAG,OAAO;CAClC,GACA,SAAS,CAAC,CAAC,MACb;CAIA,QAAO,MAHe,QAAQ,IAC5B,SAAS,KAAK,YAAY,UAAU,MAAM,SAAS;EAAE;EAAS,qBAAqB;CAAK,CAAC,CAAC,CAC5F,EAAA,CACe,QAAQ,KAAK,WAAW,MAAM,OAAO,eAAe,CAAC;AACtE;AAGA,eAAe,gBAAgF;CAC7F,MAAM,YAAY,SAAS,CAAC,CAAC,OAAO,iBAAiB;CACrD,IAAI,cAAc,MAChB,OAAO;EAAE,iBAAiB;EAAG,kBAAkB;CAAE;CAEnD,MAAM,WAAW,MAAM,IAAI,aAAa,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK;CAC5D,MAAM,YAAY,IAAI,UACpB,SAAS,CAAC,CAAC,IACX,SAAS,CAAC,CAAC,SACX;EACE,QAAQ,SAAS,CAAC,CAAC,GAAG,OAAO;EAC7B,aAAa,SAAS,CAAC,CAAC,GAAG,OAAO;EAClC,WAAW,SAAS,CAAC,CAAC,GAAG,OAAO;CAClC,GACA,SAAS,CAAC,CAAC,MACb;CACA,MAAM,UAAU,MAAM,QAAQ,IAC5B,SAAS,KAAK,YAAY,UAAU,mBAAmB,SAAS,SAAS,CAAC,CAC5E;CASA,OAAO;EAAE,iBARe,QAAQ,QAC7B,KAAa,MAAmC,MAAM,EAAE,iBACzD,CAMqB;EAAG,kBAJD,QAAQ,QAC9B,KAAa,MAAoC,MAAM,EAAE,kBAC1D,CAEuC;CAAE;AAC7C"}
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
@@ -65,7 +65,6 @@ function registerAssets(app) {
65
65
  });
66
66
  });
67
67
  }
68
- //#endregion
69
68
  export { registerAssets };
70
69
 
71
70
  //# sourceMappingURL=assets.mjs.map
@@ -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