@storyshelf/app 0.7.0 → 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 (264) 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/git-status.mjs +1 -2
  16. package/dist/hidden-data.mjs +1 -2
  17. package/dist/index.d.mts +0 -2
  18. package/dist/index.mjs +2 -3
  19. package/dist/insights/budget.mjs +1 -2
  20. package/dist/insights/deps.mjs +1 -2
  21. package/dist/insights/errors.mjs +1 -2
  22. package/dist/insights/health-panel.mjs +0 -1
  23. package/dist/insights/job.mjs +1 -2
  24. package/dist/insights/load-health.mjs +1 -2
  25. package/dist/insights/load-triage.mjs +1 -2
  26. package/dist/insights/panel.mjs +1 -2
  27. package/dist/insights/request.mjs +1 -2
  28. package/dist/insights/stale.mjs +1 -2
  29. package/dist/insights/usage-report.mjs +1 -2
  30. package/dist/insights/view.mjs +1 -2
  31. package/dist/job-canceller.mjs +1 -2
  32. package/dist/job-queue.mjs +1 -2
  33. package/dist/jobs-setup.mjs +1 -2
  34. package/dist/lifecycle.mjs +2 -3
  35. package/dist/lifecycle.mjs.map +1 -1
  36. package/dist/middleware/auth-gate.mjs +1 -2
  37. package/dist/middleware/csrf.mjs +1 -2
  38. package/dist/middleware/rate-limit.mjs +1 -2
  39. package/dist/middleware/request-log.mjs +1 -2
  40. package/dist/middleware/require-review.mjs +1 -2
  41. package/dist/middleware/require-testing.mjs +1 -2
  42. package/dist/middleware/server-timing.mjs +1 -2
  43. package/dist/middleware/setup-gate.mjs +1 -2
  44. package/dist/middleware/store-scope.mjs +1 -2
  45. package/dist/notifications-pause.mjs +1 -2
  46. package/dist/notify.d.mts +1 -3
  47. package/dist/notify.mjs +1 -2
  48. package/dist/pages/a11y-violations.mjs +1 -2
  49. package/dist/pages/admin-ai.mjs +1 -2
  50. package/dist/pages/admin-system-origin.mjs +19 -10
  51. package/dist/pages/admin-system-origin.mjs.map +1 -1
  52. package/dist/pages/admin-system-targets.mjs +1 -2
  53. package/dist/pages/admin-system.mjs +1 -2
  54. package/dist/pages/build-detail.mjs +1 -2
  55. package/dist/pages/build-diff-actions.mjs +1 -2
  56. package/dist/pages/build-diff-comments.mjs +1 -2
  57. package/dist/pages/build-diff-default.mjs +53 -0
  58. package/dist/pages/build-diff-default.mjs.map +1 -0
  59. package/dist/pages/build-diff-environment.mjs +1 -2
  60. package/dist/pages/build-diff-header.mjs +1 -2
  61. package/dist/pages/build-diff-insight.mjs +1 -2
  62. package/dist/pages/build-diff-model.mjs +20 -3
  63. package/dist/pages/build-diff-model.mjs.map +1 -1
  64. package/dist/pages/build-diff-nav.mjs +1 -2
  65. package/dist/pages/build-diff-reference.mjs +84 -0
  66. package/dist/pages/build-diff-reference.mjs.map +1 -0
  67. package/dist/pages/build-diff-shortcuts.mjs +1 -2
  68. package/dist/pages/build-diff-stale.mjs +1 -2
  69. package/dist/pages/build-diff-usage.mjs +1 -2
  70. package/dist/pages/build-diff-viewer.mjs +38 -12
  71. package/dist/pages/build-diff-viewer.mjs.map +1 -1
  72. package/dist/pages/build-diff.mjs +6 -12
  73. package/dist/pages/build-diff.mjs.map +1 -1
  74. package/dist/pages/build-tests.mjs +1 -2
  75. package/dist/pages/component-detail.mjs +1 -2
  76. package/dist/pages/component-stories.mjs +1 -2
  77. package/dist/pages/compute-jobs.mjs +1 -2
  78. package/dist/pages/design-links.mjs +1 -2
  79. package/dist/pages/environment-change.mjs +1 -2
  80. package/dist/pages/health-panel.mjs +1 -2
  81. package/dist/pages/invite.mjs +1 -2
  82. package/dist/pages/label-detail.mjs +1 -2
  83. package/dist/pages/library-grid.mjs +1 -2
  84. package/dist/pages/library-model.mjs +1 -2
  85. package/dist/pages/library-snapshots.mjs +1 -2
  86. package/dist/pages/library.mjs +1 -2
  87. package/dist/pages/login.mjs +1 -2
  88. package/dist/pages/notifications-paused.mjs +1 -2
  89. package/dist/pages/passkey-ceremony.mjs +1 -2
  90. package/dist/pages/profile.mjs +1 -2
  91. package/dist/pages/project-builds-table.mjs +1 -2
  92. package/dist/pages/project-builds.mjs +1 -2
  93. package/dist/pages/project-create.mjs +1 -2
  94. package/dist/pages/project-settings.mjs +1 -2
  95. package/dist/pages/projects-card.mjs +1 -2
  96. package/dist/pages/projects.mjs +1 -2
  97. package/dist/pages/review-changeset.mjs +1 -2
  98. package/dist/pages/review-comments.mjs +1 -2
  99. package/dist/pages/review-verdict.mjs +1 -2
  100. package/dist/pages/root.mjs +1 -2
  101. package/dist/pages/settings-ai.mjs +1 -2
  102. package/dist/pages/settings-capture.mjs +1 -2
  103. package/dist/pages/settings-design-credential.mjs +1 -2
  104. package/dist/pages/settings-design-links.mjs +1 -2
  105. package/dist/pages/settings-design.mjs +1 -2
  106. package/dist/pages/settings-general.mjs +1 -2
  107. package/dist/pages/settings-labels.mjs +1 -2
  108. package/dist/pages/settings-links.mjs +1 -2
  109. package/dist/pages/settings-members.mjs +1 -2
  110. package/dist/pages/settings-notifications-git.mjs +1 -2
  111. package/dist/pages/settings-notifications.mjs +1 -2
  112. package/dist/pages/settings-tests.mjs +1 -2
  113. package/dist/pages/settings-tokens.mjs +1 -2
  114. package/dist/pages/settings-webhooks.mjs +1 -2
  115. package/dist/pages/storybook.mjs +1 -2
  116. package/dist/pages/target-choices.mjs +1 -2
  117. package/dist/publish-setup.mjs +1 -2
  118. package/dist/retention-timer.mjs +10 -11
  119. package/dist/retention-timer.mjs.map +1 -1
  120. package/dist/review-events.mjs +1 -2
  121. package/dist/routers/admin-pages.mjs +1 -2
  122. package/dist/routers/admin-public-origin.mjs +10 -3
  123. package/dist/routers/admin-public-origin.mjs.map +1 -1
  124. package/dist/routers/admin.mjs +8 -4
  125. package/dist/routers/admin.mjs.map +1 -1
  126. package/dist/routers/assets.mjs +1 -2
  127. package/dist/routers/attempts.mjs +1 -2
  128. package/dist/routers/auth-sync.mjs +1 -2
  129. package/dist/routers/auth.mjs +1 -2
  130. package/dist/routers/baseline-guard.mjs +2 -3
  131. package/dist/routers/baseline-guard.mjs.map +1 -1
  132. package/dist/routers/baseline-reference.mjs +72 -0
  133. package/dist/routers/baseline-reference.mjs.map +1 -0
  134. package/dist/routers/baselines.handlers.mjs +1 -2
  135. package/dist/routers/baselines.mjs +1 -2
  136. package/dist/routers/build-cancel.mjs +1 -2
  137. package/dist/routers/build-changeset.mjs +1 -2
  138. package/dist/routers/build-reviews.handlers.mjs +1 -2
  139. package/dist/routers/build-reviews.mjs +1 -2
  140. package/dist/routers/builds.handlers.mjs +23 -9
  141. package/dist/routers/builds.handlers.mjs.map +1 -1
  142. package/dist/routers/builds.mjs +7 -3
  143. package/dist/routers/builds.mjs.map +1 -1
  144. package/dist/routers/capture-cache.mjs +29 -0
  145. package/dist/routers/capture-cache.mjs.map +1 -0
  146. package/dist/routers/capture-config.mjs +1 -2
  147. package/dist/routers/comments.mjs +1 -2
  148. package/dist/routers/comparison-image.mjs +29 -0
  149. package/dist/routers/comparison-image.mjs.map +1 -0
  150. package/dist/routers/component-catalog.handlers.mjs +1 -2
  151. package/dist/routers/component.handlers.mjs +1 -2
  152. package/dist/routers/connections.mjs +1 -2
  153. package/dist/routers/design-connect.handlers.mjs +1 -2
  154. package/dist/routers/design.handlers.mjs +1 -2
  155. package/dist/routers/flash.mjs +1 -2
  156. package/dist/routers/health-report.mjs +1 -2
  157. package/dist/routers/health.mjs +1 -2
  158. package/dist/routers/helpers.mjs +1 -2
  159. package/dist/routers/htmx.mjs +1 -2
  160. package/dist/routers/insights-ui.mjs +1 -2
  161. package/dist/routers/insights.handlers.mjs +1 -2
  162. package/dist/routers/insights.mjs +1 -2
  163. package/dist/routers/labels.mjs +1 -2
  164. package/dist/routers/links.handlers.mjs +1 -2
  165. package/dist/routers/links.mjs +1 -2
  166. package/dist/routers/media.mjs +14 -5
  167. package/dist/routers/media.mjs.map +1 -1
  168. package/dist/routers/members.mjs +1 -2
  169. package/dist/routers/notification-channels.mjs +1 -2
  170. package/dist/routers/notification-subscriptions.mjs +1 -2
  171. package/dist/routers/oauth-state.mjs +1 -2
  172. package/dist/routers/preview-base.mjs +1 -2
  173. package/dist/routers/preview-token.mjs +1 -2
  174. package/dist/routers/profile.mjs +1 -2
  175. package/dist/routers/projects.mjs +1 -2
  176. package/dist/routers/publish-upload.mjs +19 -12
  177. package/dist/routers/publish-upload.mjs.map +1 -1
  178. package/dist/routers/published-access.mjs +1 -2
  179. package/dist/routers/published-compression.mjs +109 -0
  180. package/dist/routers/published-compression.mjs.map +1 -0
  181. package/dist/routers/published-files.mjs +26 -9
  182. package/dist/routers/published-files.mjs.map +1 -1
  183. package/dist/routers/review-forms.mjs +1 -2
  184. package/dist/routers/review-next.mjs +1 -2
  185. package/dist/routers/review-section.handlers.mjs +1 -2
  186. package/dist/routers/review-switch-off.mjs +1 -2
  187. package/dist/routers/schemas.mjs +1 -2
  188. package/dist/routers/scoped-models.mjs +1 -2
  189. package/dist/routers/settings-ai.mjs +1 -2
  190. package/dist/routers/settings-capture.mjs +1 -2
  191. package/dist/routers/settings-design.mjs +1 -2
  192. package/dist/routers/settings-general.mjs +1 -2
  193. package/dist/routers/settings-git-channel.mjs +1 -2
  194. package/dist/routers/settings-labels.mjs +1 -2
  195. package/dist/routers/settings-links.mjs +1 -2
  196. package/dist/routers/settings-members.mjs +1 -2
  197. package/dist/routers/settings-notifications.mjs +1 -2
  198. package/dist/routers/settings-tokens.mjs +1 -2
  199. package/dist/routers/settings-webhooks.mjs +1 -2
  200. package/dist/routers/settings.handlers.mjs +1 -2
  201. package/dist/routers/settings.mjs +1 -2
  202. package/dist/routers/signed-storybook.mjs +5 -3
  203. package/dist/routers/signed-storybook.mjs.map +1 -1
  204. package/dist/routers/snapshot-groups.mjs +1 -2
  205. package/dist/routers/snapshots.mjs +1 -2
  206. package/dist/routers/storybook.mjs +21 -17
  207. package/dist/routers/storybook.mjs.map +1 -1
  208. package/dist/routers/tokens.mjs +1 -2
  209. package/dist/routers/ui.mjs +4 -2
  210. package/dist/routers/ui.mjs.map +1 -1
  211. package/dist/routers/webhooks.mjs +1 -2
  212. package/dist/routers/well-known.mjs +1 -2
  213. package/dist/routes/api-routes.mjs +1 -2
  214. package/dist/routes/classification.mjs +1 -2
  215. package/dist/routes/openapi-classes.mjs +1 -2
  216. package/dist/routes/page-routes.mjs +2 -3
  217. package/dist/routes/page-routes.mjs.map +1 -1
  218. package/dist/runtime.mjs +1 -2
  219. package/dist/serverless.mjs +1 -2
  220. package/dist/store.mjs +1 -2
  221. package/dist/targets.mjs +1 -2
  222. package/dist/ui/avatar.mjs +1 -2
  223. package/dist/ui/buttons.mjs +1 -2
  224. package/dist/ui/code-block.mjs +1 -2
  225. package/dist/ui/compare-stage.mjs +86 -54
  226. package/dist/ui/compare-stage.mjs.map +1 -1
  227. package/dist/ui/csrf-field.mjs +1 -2
  228. package/dist/ui/css.mjs +1 -2
  229. package/dist/ui/design-reference.mjs +1 -2
  230. package/dist/ui/document.mjs +1 -2
  231. package/dist/ui/dropdown.mjs +1 -2
  232. package/dist/ui/feedback.mjs +1 -2
  233. package/dist/ui/filter-input.mjs +1 -2
  234. package/dist/ui/forms.mjs +1 -2
  235. package/dist/ui/icons/icon.mjs +1 -2
  236. package/dist/ui/icons/paths.mjs +1 -2
  237. package/dist/ui/icons/sprite.mjs +1 -2
  238. package/dist/ui/kbd.mjs +1 -2
  239. package/dist/ui/layout.mjs +1 -2
  240. package/dist/ui/preview-embed.mjs +1 -2
  241. package/dist/ui/progress.mjs +1 -2
  242. package/dist/ui/segmented.mjs +1 -2
  243. package/dist/ui/shell/account.mjs +1 -2
  244. package/dist/ui/shell/bare.mjs +1 -2
  245. package/dist/ui/shell/boot-script.mjs +1 -2
  246. package/dist/ui/shell/client-script.mjs +1 -2
  247. package/dist/ui/shell/nav.mjs +1 -2
  248. package/dist/ui/shell/review-script.mjs +17 -3
  249. package/dist/ui/shell/review-script.mjs.map +1 -1
  250. package/dist/ui/shell/sidebar.mjs +1 -2
  251. package/dist/ui/shell/topbar.mjs +1 -2
  252. package/dist/ui/stacks.mjs +1 -2
  253. package/dist/ui/styles/base.mjs +1 -2
  254. package/dist/ui/styles/behavior.mjs +1 -2
  255. package/dist/ui/styles/review-layout.mjs +1 -2
  256. package/dist/ui/styles/review.mjs +1 -2
  257. package/dist/ui/styles/tokens.mjs +1 -2
  258. package/dist/ui/styles.mjs +1 -2
  259. package/dist/ui/subnav.mjs +1 -2
  260. package/dist/ui/table.mjs +1 -2
  261. package/dist/ui/theme.mjs +1 -2
  262. package/dist/ui/thumbnail.mjs +1 -2
  263. package/dist/ui/time.mjs +1 -2
  264. package/package.json +9 -9
@@ -1 +1 @@
1
- {"version":3,"file":"lifecycle.mjs","names":[],"sources":["../src/lifecycle.ts"],"sourcesContent":["import type { AdapterSetupContext } from \"@storyshelf/core/adapter/metadata\";\nimport {\n AdapterLifecycleError,\n bindAdapterLoggers,\n adapterSourcesOf,\n collectSetups,\n collectTeardowns,\n runAdapterSetups,\n runAdapterTeardowns,\n validateBootAssembly,\n} from \"@storyshelf/core/adapter/setup\";\nimport type { AdapterSetupResult } from \"@storyshelf/core/adapter/setup\";\nimport type { CaptureConsumer } from \"@storyshelf/core/capture\";\nimport type { ShelfOptions } from \"@storyshelf/core/config\";\nimport { shutdownDiffPool, verifyImageEngine } from \"@storyshelf/core/diff\";\nimport type { Logger } from \"@storyshelf/core/logger\";\nimport { purgeAiData } from \"@storyshelf/core/retention\";\nimport { sanitizeErrorText } from \"@storyshelf/core/utils\";\nimport type { ShelfRouter, ShelfLifecycle } from \"./app-types.ts\";\nimport { settleInsightJobs } from \"./insights/job.ts\";\nimport { startBranchGcTimer } from \"./retention-timer.ts\";\nimport type { ServerRuntime } from \"./runtime.ts\";\n\n/** Mutable setup-settlement cell shared by the gate, health, and `setup()`. */\nexport interface LifecycleCell {\n ready: Promise<AdapterSetupResult>;\n settled: AdapterSetupResult | null;\n /** Fire-and-forget boot tasks (recovery, credential check) that teardown must outwait. */\n background?: Promise<unknown>[];\n /** Override of {@link BACKGROUND_DRAIN_MS} (tests). */\n drainMs?: number;\n /** The job consumer (null on hosts that run none); started once adapters are ready, drained by `teardown()`. */\n consumer?: CaptureConsumer | null;\n /** The running consumer session, settled once it was stopped and drained. */\n consumerRun?: Promise<void>;\n /** Teardown ran: the consumer stays stopped (a late \"adapters are ready\" must not restart it). */\n consumerStopped?: boolean;\n}\n\n/** Longest teardown waits for background boot tasks before closing adapters anyway. */\nconst BACKGROUND_DRAIN_MS = 5000;\n\n/** Register a boot task so `teardown()` does not close adapters underneath it. */\nexport function trackBackground(cell: LifecycleCell, task: Promise<unknown>): void {\n (cell.background ??= []).push(task);\n}\n\n/** Start the job consumer once adapters are set up; a no-op when none exists or it runs. */\nexport function startConsumer(cell: LifecycleCell, logger: Logger): void {\n const consumer = cell.consumer;\n if (!consumer || consumer.isRunning() || cell.consumerStopped) {\n return;\n }\n cell.consumerRun = consumer.start().catch((error: unknown) => {\n logger.error({ err: error }, \"job consumer stopped\");\n });\n}\n\n/** Stop polling and wait for the jobs in flight to finish (the SIGTERM drain). */\nasync function stopConsumer(cell: LifecycleCell): Promise<void> {\n cell.consumerStopped = true;\n await cell.consumer?.stop();\n await cell.consumerRun;\n}\n\n/** Wait for tracked boot tasks, bounded so a hung task can never block shutdown. */\nasync function drainBackground(cell: LifecycleCell): Promise<void> {\n const pending = Promise.allSettled(cell.background ?? []);\n let timer: NodeJS.Timeout | undefined;\n const timeout = new Promise<void>((resolve) => {\n timer = setTimeout(resolve, cell.drainMs ?? BACKGROUND_DRAIN_MS);\n timer.unref();\n });\n await Promise.race([pending, timeout]);\n clearTimeout(timer);\n}\n\nfunction trackSettlement(cell: LifecycleCell, promise: Promise<AdapterSetupResult>): void {\n promise.then(\n (result) => {\n cell.settled = result;\n },\n () => {\n cell.settled = { ok: false, failures: [] };\n },\n );\n}\n\n/** First failing boot check (assembly, auth, AI), or null when all pass. */\nasync function firstBootFailure(\n options: ShelfOptions,\n logger: Logger,\n): Promise<AdapterSetupResult | null> {\n const invalid = validateBootAssembly(adapterSourcesOf(options));\n if (invalid.length > 0) {\n logger.error({ failures: invalid }, \"adapter validation failed\");\n return { ok: false, failures: invalid };\n }\n for (const [label, run] of [\n [\"auth\", runAuthSetup],\n [\"ai\", runAiSetup],\n [\"images\", runImagesSetup],\n ] as const) {\n // eslint-disable-next-line no-await-in-loop -- boot checks run in order and stop at the first failure\n const failed = await run(options);\n if (failed) {\n logger.error({ failures: failed.failures }, `${label} setup failed`);\n return failed;\n }\n }\n return null;\n}\n\n/** Kick the eager background setup run (resolves; never rejects). */\nfunction kickSetup(\n options: ShelfOptions,\n ctx: AdapterSetupContext,\n logger: Logger,\n cell: LifecycleCell,\n): void {\n // Auth is not an adapter: its one-shot boot validation runs first, then\n // the adapter setups. A failing secret check fails readiness, like others.\n cell.ready = (async (): Promise<AdapterSetupResult> => {\n const failed = await firstBootFailure(options, logger);\n return (\n failed ?? (await runAdapterSetups(collectSetups(adapterSourcesOf(options)), ctx, logger))\n );\n })();\n trackSettlement(cell, cell.ready);\n}\n\n/** Login method count, or null when the engine throws during introspection. */\nfunction loginMethodCount(auth: NonNullable<ShelfOptions[\"auth\"]>): number | null {\n try {\n return auth.loginMethods().length;\n } catch {\n return null;\n }\n}\n\n/** Log which auth mode the server boots with (open servers stay visible). */\nfunction logAuthMode(options: ShelfOptions, logger: Logger): void {\n if (!options.auth) {\n logger.info(\"auth disabled (local default; every route is open)\");\n return;\n }\n const methods = loginMethodCount(options.auth);\n if (methods === null) {\n logger.warn(\"auth loginMethods() threw during boot logging\");\n return;\n }\n logger.info({ loginMethods: methods }, \"auth enabled\");\n}\n\n/** Kick eager setup and attach the `app.lifecycle` namespace (single run). */\nexport function attachLifecycle(\n app: ShelfRouter,\n options: ShelfOptions,\n runtime: ServerRuntime,\n cell: LifecycleCell,\n): void {\n const setupCtx: AdapterSetupContext = { config: runtime.config, logger: runtime.logger };\n bindAdapterLoggers(options, runtime.logger);\n options.ai?.setLogger?.(runtime.logger.child({ component: \"ai\" }));\n logAuthMode(options, runtime.logger);\n kickSetup(options, setupCtx, runtime.logger, cell);\n const timer = startBranchGcInterval(options, runtime);\n Object.assign(app, {\n lifecycle: createLifecycle(options, setupCtx, runtime.logger, cell, timer),\n });\n}\n\nfunction startBranchGcInterval(\n options: ShelfOptions,\n runtime: ServerRuntime,\n): { stop(): void } | null {\n return startBranchGcTimer(options.database, options.storage, runtime.config, runtime.logger);\n}\n\n/** Run auth boot validation, returning a failure result instead of throwing. */\nasync function runAuthSetup(options: ShelfOptions): Promise<AdapterSetupResult | null> {\n try {\n await options.auth?.setup?.();\n return null;\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return {\n ok: false,\n failures: [\n { category: \"auth\", kind: \"auth\", name: \"auth\", error: sanitizeErrorText(message, 500) },\n ],\n };\n }\n}\n/** Run AI boot checks and the stale-run sweep; failure fails readiness like auth. */\nasync function runAiSetup(options: ShelfOptions): Promise<AdapterSetupResult | null> {\n if (!options.ai) {\n return null;\n }\n try {\n await options.ai.setup();\n await purgeAiData(options.database);\n return null;\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return {\n ok: false,\n failures: [\n { category: \"ai\", kind: \"ai\", name: \"ai\", error: sanitizeErrorText(message, 500) },\n ],\n };\n }\n}\n/** Load the configured image engine now, so a missing `sharp` fails readiness instead of the first diff. */\nasync function runImagesSetup(): Promise<AdapterSetupResult | null> {\n try {\n await verifyImageEngine();\n return null;\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return {\n ok: false,\n failures: [\n {\n category: \"images\",\n kind: \"images\",\n name: \"images\",\n error: sanitizeErrorText(message, 500),\n },\n ],\n };\n }\n}\n/** Validation failure settled on the cell — null when the assembly is sound. */\nfunction invalidSources(options: ShelfOptions, cell: LifecycleCell): AdapterSetupResult | null {\n const invalid = validateBootAssembly(adapterSourcesOf(options));\n if (invalid.length === 0) {\n return null;\n }\n const result: AdapterSetupResult = { ok: false, failures: invalid };\n cell.ready = Promise.resolve(result);\n cell.settled = result;\n return result;\n}\n/** Build the `app.lifecycle` namespace closing over one settlement cell. */\nfunction createLifecycle(\n options: ShelfOptions,\n ctx: AdapterSetupContext,\n logger: Logger,\n cell: LifecycleCell,\n timer: { stop(): void } | null = null,\n): ShelfLifecycle {\n return {\n get ready() {\n return cell.ready;\n },\n logger,\n setup: async () => {\n const blocked = invalidSources(options, cell);\n if (blocked) {\n throw new AdapterLifecycleError(\"setup\", blocked.failures);\n }\n const result = await runAdapterSetups(collectSetups(adapterSourcesOf(options)), ctx, logger);\n cell.ready = Promise.resolve(result);\n cell.settled = result;\n if (!result.ok) {\n throw new AdapterLifecycleError(\"setup\", result.failures);\n }\n cell.consumerStopped = false;\n startConsumer(cell, logger);\n },\n teardown: async () => {\n timer?.stop();\n await stopConsumer(cell);\n await drainBackground(cell);\n await settleInsightJobs();\n await options.ai?.teardown();\n await shutdownDiffPool();\n const result = await runAdapterTeardowns(\n collectTeardowns(adapterSourcesOf(options)),\n ctx,\n logger,\n );\n await options.observability?.shutdown().catch((error: unknown) => {\n logger.error({ err: error }, \"observability shutdown failed\");\n });\n if (!result.ok) {\n throw new AdapterLifecycleError(\"teardown\", result.failures);\n }\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"lifecycle.mjs","names":[],"sources":["../src/lifecycle.ts"],"sourcesContent":["import type { AdapterSetupContext } from \"@storyshelf/core/adapter/metadata\";\nimport {\n AdapterLifecycleError,\n bindAdapterLoggers,\n adapterSourcesOf,\n collectSetups,\n collectTeardowns,\n runAdapterSetups,\n runAdapterTeardowns,\n validateBootAssembly,\n} from \"@storyshelf/core/adapter/setup\";\nimport type { AdapterSetupResult } from \"@storyshelf/core/adapter/setup\";\nimport type { CaptureConsumer } from \"@storyshelf/core/capture\";\nimport type { ShelfOptions } from \"@storyshelf/core/config\";\nimport { shutdownDiffPool, verifyImageEngine } from \"@storyshelf/core/diff\";\nimport type { Logger } from \"@storyshelf/core/logger\";\nimport { purgeAiData } from \"@storyshelf/core/retention\";\nimport { sanitizeErrorText } from \"@storyshelf/core/utils\";\nimport type { ShelfRouter, ShelfLifecycle } from \"./app-types.ts\";\nimport { settleInsightJobs } from \"./insights/job.ts\";\nimport { startBranchGcTimer } from \"./retention-timer.ts\";\nimport type { ServerRuntime } from \"./runtime.ts\";\n\n/** Mutable setup-settlement cell shared by the gate, health, and `setup()`. */\nexport interface LifecycleCell {\n ready: Promise<AdapterSetupResult>;\n settled: AdapterSetupResult | null;\n /** Fire-and-forget boot tasks (recovery, credential check) that teardown must outwait. */\n background?: Promise<unknown>[];\n /** Override of {@link BACKGROUND_DRAIN_MS} (tests). */\n drainMs?: number;\n /** The job consumer (null on hosts that run none); started once adapters are ready, drained by `teardown()`. */\n consumer?: CaptureConsumer | null;\n /** The running consumer session, settled once it was stopped and drained. */\n consumerRun?: Promise<void>;\n /** Teardown ran: the consumer stays stopped (a late \"adapters are ready\" must not restart it). */\n consumerStopped?: boolean;\n}\n\n/** Longest teardown waits for background boot tasks before closing adapters anyway. */\nconst BACKGROUND_DRAIN_MS = 5000;\n\n/** Register a boot task so `teardown()` does not close adapters underneath it. */\nexport function trackBackground(cell: LifecycleCell, task: Promise<unknown>): void {\n (cell.background ??= []).push(task);\n}\n\n/** Start the job consumer once adapters are set up; a no-op when none exists or it runs. */\nexport function startConsumer(cell: LifecycleCell, logger: Logger): void {\n const consumer = cell.consumer;\n if (!consumer || consumer.isRunning() || cell.consumerStopped) {\n return;\n }\n cell.consumerRun = consumer.start().catch((error: unknown) => {\n logger.error({ err: error }, \"job consumer stopped\");\n });\n}\n\n/** Stop polling and wait for the jobs in flight to finish (the SIGTERM drain). */\nasync function stopConsumer(cell: LifecycleCell): Promise<void> {\n cell.consumerStopped = true;\n await cell.consumer?.stop();\n await cell.consumerRun;\n}\n\n/** Wait for tracked boot tasks, bounded so a hung task can never block shutdown. */\nasync function drainBackground(cell: LifecycleCell): Promise<void> {\n const pending = Promise.allSettled(cell.background ?? []);\n let timer: NodeJS.Timeout | undefined;\n const timeout = new Promise<void>((resolve) => {\n timer = setTimeout(resolve, cell.drainMs ?? BACKGROUND_DRAIN_MS);\n timer.unref();\n });\n await Promise.race([pending, timeout]);\n clearTimeout(timer);\n}\n\nfunction trackSettlement(cell: LifecycleCell, promise: Promise<AdapterSetupResult>): void {\n promise.then(\n (result) => {\n cell.settled = result;\n },\n () => {\n cell.settled = { ok: false, failures: [] };\n },\n );\n}\n\n/** First failing boot check (assembly, auth, AI), or null when all pass. */\nasync function firstBootFailure(\n options: ShelfOptions,\n logger: Logger,\n): Promise<AdapterSetupResult | null> {\n const invalid = validateBootAssembly(adapterSourcesOf(options));\n if (invalid.length > 0) {\n logger.error({ failures: invalid }, \"adapter validation failed\");\n return { ok: false, failures: invalid };\n }\n for (const [label, run] of [\n [\"auth\", runAuthSetup],\n [\"ai\", runAiSetup],\n [\"images\", runImagesSetup],\n ] as const) {\n // eslint-disable-next-line no-await-in-loop -- boot checks run in order and stop at the first failure\n const failed = await run(options);\n if (failed) {\n logger.error({ failures: failed.failures }, `${label} setup failed`);\n return failed;\n }\n }\n return null;\n}\n\n/** Kick the eager background setup run (resolves; never rejects). */\nfunction kickSetup(\n options: ShelfOptions,\n ctx: AdapterSetupContext,\n logger: Logger,\n cell: LifecycleCell,\n): void {\n // Auth is not an adapter: its one-shot boot validation runs first, then\n // the adapter setups. A failing secret check fails readiness, like others.\n cell.ready = (async (): Promise<AdapterSetupResult> => {\n const failed = await firstBootFailure(options, logger);\n return (\n failed ?? (await runAdapterSetups(collectSetups(adapterSourcesOf(options)), ctx, logger))\n );\n })();\n trackSettlement(cell, cell.ready);\n}\n\n/** Login method count, or null when the engine throws during introspection. */\nfunction loginMethodCount(auth: NonNullable<ShelfOptions[\"auth\"]>): number | null {\n try {\n return auth.loginMethods().length;\n } catch {\n return null;\n }\n}\n\n/** Log which auth mode the server boots with (open servers stay visible). */\nfunction logAuthMode(options: ShelfOptions, logger: Logger): void {\n if (!options.auth) {\n logger.info(\"auth disabled (local default; every route is open)\");\n return;\n }\n const methods = loginMethodCount(options.auth);\n if (methods === null) {\n logger.warn(\"auth loginMethods() threw during boot logging\");\n return;\n }\n logger.info({ loginMethods: methods }, \"auth enabled\");\n}\n\n/** Kick eager setup and attach the `app.lifecycle` namespace (single run). */\nexport function attachLifecycle(\n app: ShelfRouter,\n options: ShelfOptions,\n runtime: ServerRuntime,\n cell: LifecycleCell,\n): void {\n const setupCtx: AdapterSetupContext = { config: runtime.config, logger: runtime.logger };\n bindAdapterLoggers(options, runtime.logger);\n options.ai?.setLogger?.(runtime.logger.child({ component: \"ai\" }));\n logAuthMode(options, runtime.logger);\n kickSetup(options, setupCtx, runtime.logger, cell);\n const timer = startBranchGcInterval(options, runtime);\n Object.assign(app, {\n lifecycle: createLifecycle(options, setupCtx, runtime.logger, cell, timer),\n });\n}\n\nfunction startBranchGcInterval(\n options: ShelfOptions,\n runtime: ServerRuntime,\n): { stop(): void } | null {\n return startBranchGcTimer(\n options.database,\n options.storage,\n runtime.config,\n runtime.logger,\n Boolean(options.testing),\n );\n}\n\n/** Run auth boot validation, returning a failure result instead of throwing. */\nasync function runAuthSetup(options: ShelfOptions): Promise<AdapterSetupResult | null> {\n try {\n await options.auth?.setup?.();\n return null;\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return {\n ok: false,\n failures: [\n { category: \"auth\", kind: \"auth\", name: \"auth\", error: sanitizeErrorText(message, 500) },\n ],\n };\n }\n}\n/** Run AI boot checks and the stale-run sweep; failure fails readiness like auth. */\nasync function runAiSetup(options: ShelfOptions): Promise<AdapterSetupResult | null> {\n if (!options.ai) {\n return null;\n }\n try {\n await options.ai.setup();\n await purgeAiData(options.database);\n return null;\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return {\n ok: false,\n failures: [\n { category: \"ai\", kind: \"ai\", name: \"ai\", error: sanitizeErrorText(message, 500) },\n ],\n };\n }\n}\n/** Load the configured image engine now, so a missing `sharp` fails readiness instead of the first diff. */\nasync function runImagesSetup(): Promise<AdapterSetupResult | null> {\n try {\n await verifyImageEngine();\n return null;\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return {\n ok: false,\n failures: [\n {\n category: \"images\",\n kind: \"images\",\n name: \"images\",\n error: sanitizeErrorText(message, 500),\n },\n ],\n };\n }\n}\n/** Validation failure settled on the cell — null when the assembly is sound. */\nfunction invalidSources(options: ShelfOptions, cell: LifecycleCell): AdapterSetupResult | null {\n const invalid = validateBootAssembly(adapterSourcesOf(options));\n if (invalid.length === 0) {\n return null;\n }\n const result: AdapterSetupResult = { ok: false, failures: invalid };\n cell.ready = Promise.resolve(result);\n cell.settled = result;\n return result;\n}\n/** Build the `app.lifecycle` namespace closing over one settlement cell. */\nfunction createLifecycle(\n options: ShelfOptions,\n ctx: AdapterSetupContext,\n logger: Logger,\n cell: LifecycleCell,\n timer: { stop(): void } | null = null,\n): ShelfLifecycle {\n return {\n get ready() {\n return cell.ready;\n },\n logger,\n setup: async () => {\n const blocked = invalidSources(options, cell);\n if (blocked) {\n throw new AdapterLifecycleError(\"setup\", blocked.failures);\n }\n const result = await runAdapterSetups(collectSetups(adapterSourcesOf(options)), ctx, logger);\n cell.ready = Promise.resolve(result);\n cell.settled = result;\n if (!result.ok) {\n throw new AdapterLifecycleError(\"setup\", result.failures);\n }\n cell.consumerStopped = false;\n startConsumer(cell, logger);\n },\n teardown: async () => {\n timer?.stop();\n await stopConsumer(cell);\n await drainBackground(cell);\n await settleInsightJobs();\n await options.ai?.teardown();\n await shutdownDiffPool();\n const result = await runAdapterTeardowns(\n collectTeardowns(adapterSourcesOf(options)),\n ctx,\n logger,\n );\n await options.observability?.shutdown().catch((error: unknown) => {\n logger.error({ err: error }, \"observability shutdown failed\");\n });\n if (!result.ok) {\n throw new AdapterLifecycleError(\"teardown\", result.failures);\n }\n },\n };\n}\n"],"mappings":"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"}
@@ -3,7 +3,7 @@ import "node:url";
3
3
  import.meta.url;
4
4
  import { getStore } from "../store.mjs";
5
5
  import { STORYBOOK_TOKEN_PREFIX } from "@storyshelf/core/utils";
6
- //#region src/middleware/auth-gate.ts
6
+ // src/middleware/auth-gate.ts
7
7
  /** Resolve the request user from the auth adapter (null when auth is off or a token is used). */
8
8
  async function resolveRequestUser(c, auth) {
9
9
  if (!auth) return null;
@@ -32,7 +32,6 @@ function authGate() {
32
32
  return c.redirect("/auth/login", 302);
33
33
  };
34
34
  }
35
- //#endregion
36
35
  export { authGate, resolveRequestUser };
37
36
 
38
37
  //# sourceMappingURL=auth-gate.mjs.map
@@ -4,7 +4,7 @@ import.meta.url;
4
4
  import { timingSafeEqualString } from "@storyshelf/core/utils";
5
5
  import { SESSION_COOKIE } from "@storyshelf/core/types";
6
6
  import { createHash, randomBytes } from "node:crypto";
7
- //#region src/middleware/csrf.ts
7
+ // src/middleware/csrf.ts
8
8
  const DEFAULT_SESSION = "default";
9
9
  const FALLBACK_SECRET = randomBytes(32).toString("hex");
10
10
  /** Use the app secret when provided so tokens survive restarts and instances. */
@@ -69,7 +69,6 @@ function csrf(secret) {
69
69
  function getCsrfToken(secret, sessionId = DEFAULT_SESSION) {
70
70
  return generateToken(resolveSecret(secret), sessionId);
71
71
  }
72
- //#endregion
73
72
  export { csrf, getCsrfToken, sessionIdFrom };
74
73
 
75
74
  //# sourceMappingURL=csrf.mjs.map
@@ -1,7 +1,7 @@
1
1
  import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
- //#region src/middleware/rate-limit.ts
4
+ // src/middleware/rate-limit.ts
5
5
  /** Direct peer IP from server runtimes exposing the socket (unspoofable TCP source). */
6
6
  function peerIp(c) {
7
7
  const env = c.env ?? {};
@@ -70,7 +70,6 @@ function createRateLimiter() {
70
70
  };
71
71
  };
72
72
  }
73
- //#endregion
74
73
  export { createRateLimiter, rateLimitKey };
75
74
 
76
75
  //# sourceMappingURL=rate-limit.mjs.map
@@ -2,7 +2,7 @@ import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
4
  import { redactSecrets } from "@storyshelf/core/utils";
5
- //#region src/middleware/request-log.ts
5
+ // src/middleware/request-log.ts
6
6
  /** Hono middleware logging request start/end with duration. */
7
7
  function requestLogging(logger) {
8
8
  return async (c, next) => {
@@ -25,7 +25,6 @@ function requestLogging(logger) {
25
25
  }, "request end");
26
26
  };
27
27
  }
28
- //#endregion
29
28
  export { requestLogging };
30
29
 
31
30
  //# sourceMappingURL=request-log.mjs.map
@@ -3,7 +3,7 @@ import "node:url";
3
3
  import.meta.url;
4
4
  import { matchedRouteClass } from "./require-testing.mjs";
5
5
  import { resolveAuthorizedProject } from "../routers/helpers.mjs";
6
- //#region src/middleware/require-review.ts
6
+ // src/middleware/require-review.ts
7
7
  /** Body of the 404 a `review` route answers for a project with review off. */
8
8
  const REVIEW_DISABLED_MESSAGE = "Review is off for this project.";
9
9
  /** Roles that may see a project's routes at all; the gate answers only to them. */
@@ -45,7 +45,6 @@ async function reviewIsOff(c) {
45
45
  return false;
46
46
  }
47
47
  }
48
- //#endregion
49
48
  export { REVIEW_DISABLED_MESSAGE, requireReview };
50
49
 
51
50
  //# sourceMappingURL=require-review.mjs.map
@@ -3,7 +3,7 @@ import "node:url";
3
3
  import.meta.url;
4
4
  import { classOfRoute } from "../routes/classification.mjs";
5
5
  import { matchedRoutes } from "hono/route";
6
- //#region src/middleware/require-testing.ts
6
+ // src/middleware/require-testing.ts
7
7
  /** Body of the 404 a `testing` route answers on a hosting-only instance. */
8
8
  const TESTING_DISABLED_MESSAGE = "Testing is not enabled on this instance.";
9
9
  /**
@@ -35,7 +35,6 @@ function matchedRouteClass(c) {
35
35
  function isTestingRoute(c) {
36
36
  return matchedRouteClass(c) === "testing";
37
37
  }
38
- //#endregion
39
38
  export { TESTING_DISABLED_MESSAGE, matchedRouteClass, requireTesting };
40
39
 
41
40
  //# sourceMappingURL=require-testing.mjs.map
@@ -3,7 +3,7 @@ import "node:url";
3
3
  import.meta.url;
4
4
  import { runWithTimings } from "@storyshelf/core/utils";
5
5
  import { setMetric, timing } from "hono/timing";
6
- //#region src/middleware/server-timing.ts
6
+ // src/middleware/server-timing.ts
7
7
  /**
8
8
  * Opt-in `Server-Timing` response headers over Hono's timing middleware.
9
9
  *
@@ -33,7 +33,6 @@ function serverTiming(enabled) {
33
33
  if (headers.length > 0) c.res.headers.append("Server-Timing", headers.join(","));
34
34
  };
35
35
  }
36
- //#endregion
37
36
  export { serverTiming };
38
37
 
39
38
  //# sourceMappingURL=server-timing.mjs.map
@@ -2,7 +2,7 @@ import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
4
  import { sanitizeErrorText } from "@storyshelf/core/utils";
5
- //#region src/middleware/setup-gate.ts
5
+ // src/middleware/setup-gate.ts
6
6
  /** Failures safe to expose on the setup gate's 503 (redacted, no secrets). */
7
7
  function publicFailures(result) {
8
8
  return result.failures.map((failure) => ({
@@ -31,7 +31,6 @@ function setupGate(getReady) {
31
31
  await next();
32
32
  };
33
33
  }
34
- //#endregion
35
34
  export { setupGate };
36
35
 
37
36
  //# sourceMappingURL=setup-gate.mjs.map
@@ -3,7 +3,7 @@ import "node:url";
3
3
  import.meta.url;
4
4
  import { runWithStore } from "../store.mjs";
5
5
  import { sessionIdFrom } from "./csrf.mjs";
6
- //#region src/middleware/store-scope.ts
6
+ // src/middleware/store-scope.ts
7
7
  /** Hono middleware running downstream handlers inside the request store. */
8
8
  function storeScope(deps) {
9
9
  return async (c, next) => {
@@ -37,7 +37,6 @@ function storeScope(deps) {
37
37
  });
38
38
  };
39
39
  }
40
- //#endregion
41
40
  export { storeScope };
42
41
 
43
42
  //# sourceMappingURL=store-scope.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/notifications-pause.ts
5
+ // src/notifications-pause.ts
6
6
  /** What the System page and the notification settings tell the viewer. */
7
7
  const NOTIFICATIONS_PAUSED_TEXT = "Notifications are paused until a public URL is set; set PUBLIC_BASE_URL or confirm the detected origin.";
8
8
  /** The origin notifications link to, or `undefined` while none is resolved. */
@@ -27,7 +27,6 @@ function warnPaused(logger, event) {
27
27
  function clearPaused(logger) {
28
28
  warned.delete(logger);
29
29
  }
30
- //#endregion
31
30
  export { NOTIFICATIONS_PAUSED_TEXT, clearPaused, notificationOrigin, notificationsPaused, warnPaused };
32
31
 
33
32
  //# sourceMappingURL=notifications-pause.mjs.map
package/dist/notify.d.mts CHANGED
@@ -3,16 +3,14 @@ import __tsdown_shims_url from 'node:url';
3
3
  import { ShelfConfig, UIConfig } from "@storyshelf/core/config";
4
4
  import "@storyshelf/core/models";
5
5
  import { NotifierProvider } from "@storyshelf/core/adapter/notifier";
6
- import { Logger } from "@storyshelf/core/logger";
7
6
  import { DatabaseAdapter } from "@storyshelf/core/adapter/database";
7
+ import { Logger } from "@storyshelf/core/logger";
8
8
  import "@storyshelf/core/schema";
9
- //#region src/notify.d.ts
10
9
  /**
11
10
  * Host hook for the auth engine (`onAuthSystemEvent`): fans `sys:*` auth
12
11
  * events out to admin channels. Safe to call anywhere — outside a request
13
12
  * scope (e.g. boot) it resolves to a no-op instead of throwing.
14
13
  */
15
14
  declare function createAuthSystemHook(): (event: "sys:user-created" | "sys:invite-issued", data: Record<string, unknown>) => Promise<void>;
16
- //#endregion
17
15
  export { createAuthSystemHook };
18
16
  //# sourceMappingURL=notify.d.mts.map
package/dist/notify.mjs CHANGED
@@ -6,7 +6,7 @@ import { getStore } from "./store.mjs";
6
6
  import { clearPaused, notificationOrigin, warnPaused } from "./notifications-pause.mjs";
7
7
  import { MemberModel, NotificationChannelModel, NotificationSubscriptionModel, UserModel } from "@storyshelf/core/models";
8
8
  import { emitNotifications, emitSystemNotification } from "@storyshelf/core/adapter/notifier";
9
- //#region src/notify.ts
9
+ // src/notify.ts
10
10
  /** Absolute review URL for a project page under the resolved public origin. */
11
11
  function reviewUrlFor(origin, slug, suffix) {
12
12
  return `${origin}/projects/${slug}${suffix}`;
@@ -174,7 +174,6 @@ function createAuthSystemHook() {
174
174
  function now() {
175
175
  return (/* @__PURE__ */ new Date()).toISOString();
176
176
  }
177
- //#endregion
178
177
  export { createAuthSystemHook, notifyProject, notifyProjectWith, notifySystem, notifySystemWith };
179
178
 
180
179
  //# sourceMappingURL=notify.mjs.map
@@ -3,7 +3,7 @@ import "node:url";
3
3
  import.meta.url;
4
4
  import { Alert, Badge, Meta } from "../ui/feedback.mjs";
5
5
  import { jsx, jsxs } from "hono/jsx/jsx-runtime";
6
- //#region src/pages/a11y-violations.tsx
6
+ // src/pages/a11y-violations.tsx
7
7
  /** The axe violation texts stored on a snapshot (`rule (impact): help [selector] n node(s)`); empty when none. */
8
8
  function a11yViolationsOf(snapshot) {
9
9
  if (!snapshot.a11yViolations) return [];
@@ -110,7 +110,6 @@ function A11yViolationsDetails(props) {
110
110
  ]
111
111
  }) }), violations.map((text) => /* @__PURE__ */ jsx(Meta, { children: /* @__PURE__ */ jsx("code", { children: text }) }, text))] });
112
112
  }
113
- //#endregion
114
113
  export { A11yViolationsDetails, A11yViolationsNotice, a11yCarrierOf, a11yCountOf, a11yFindingsOf, a11ySummaryNote, a11ySummaryOf, a11yUnavailableNote, a11yUnavailableReasonOf, a11yViolationsOf, isAxeUnavailable };
115
114
 
116
115
  //# sourceMappingURL=a11y-violations.mjs.map
@@ -6,7 +6,7 @@ import { Card, PageHeader, SectionTitle } from "../ui/layout.mjs";
6
6
  import { Table } from "../ui/table.mjs";
7
7
  import { DocumentLayout } from "../ui/document.mjs";
8
8
  import { Fragment, jsx, jsxs } from "hono/jsx/jsx-runtime";
9
- //#region src/pages/admin-ai.tsx
9
+ // src/pages/admin-ai.tsx
10
10
  function DaysTable(props) {
11
11
  return /* @__PURE__ */ jsx(Table, {
12
12
  dense: true,
@@ -72,7 +72,6 @@ function renderAdminAiPage(report) {
72
72
  ]
73
73
  });
74
74
  }
75
- //#endregion
76
75
  export { renderAdminAiPage };
77
76
 
78
77
  //# sourceMappingURL=admin-ai.mjs.map
@@ -8,7 +8,7 @@ import { Card, SectionTitle } from "../ui/layout.mjs";
8
8
  import { HStack } from "../ui/stacks.mjs";
9
9
  import { csrfField } from "../ui/csrf-field.mjs";
10
10
  import { Fragment, jsx, jsxs } from "hono/jsx/jsx-runtime";
11
- //#region src/pages/admin-system-origin.tsx
11
+ // src/pages/admin-system-origin.tsx
12
12
  const SOURCE_LABELS = {
13
13
  config: "configured (PUBLIC_BASE_URL)",
14
14
  platform: "detected from the hosting platform",
@@ -17,7 +17,7 @@ const SOURCE_LABELS = {
17
17
  /** Where absolute links in emails, commit statuses and signed Storybook URLs point (ADR 0007). */
18
18
  function PublicOriginCard(props) {
19
19
  const { resolved, stored, learns } = props.origin;
20
- const reports = learns && (resolved === null || resolved.source === "admin");
20
+ const fixed = resolved !== null && resolved.source !== "admin";
21
21
  return /* @__PURE__ */ jsxs(Card, { children: [
22
22
  /* @__PURE__ */ jsx(SectionTitle, { children: "Public origin" }),
23
23
  /* @__PURE__ */ jsxs(Meta, { children: [
@@ -30,11 +30,17 @@ function PublicOriginCard(props) {
30
30
  learns,
31
31
  stored
32
32
  }) : /* @__PURE__ */ jsx(Resolved, { resolved }),
33
- reports ? /* @__PURE__ */ jsx("div", { "data-report-origin": "/admin/public-origin/report" }) : null,
34
- learns && stored !== null ? /* @__PURE__ */ jsx(StoredForm, { stored }) : null,
35
- learns && stored === null ? /* @__PURE__ */ jsx(EnterForm, {}) : null
33
+ learns && !fixed ? /* @__PURE__ */ jsx("div", { "data-report-origin": "/admin/public-origin/report" }) : null,
34
+ fixed ? null : /* @__PURE__ */ jsx(OriginForms, {
35
+ learns,
36
+ stored
37
+ })
36
38
  ] });
37
39
  }
40
+ function OriginForms(props) {
41
+ if (!props.learns) return /* @__PURE__ */ jsx(Fragment, {});
42
+ return props.stored === null ? /* @__PURE__ */ jsx(EnterForm, {}) : /* @__PURE__ */ jsx(StoredForm, { stored: props.stored });
43
+ }
38
44
  function NothingResolved(props) {
39
45
  if (props.stored !== null && !props.stored.confirmed) return /* @__PURE__ */ jsxs(Alert, {
40
46
  tone: "warning",
@@ -53,10 +59,14 @@ function NothingResolved(props) {
53
59
  }
54
60
  function Resolved(props) {
55
61
  const { resolved } = props;
56
- return /* @__PURE__ */ jsxs(HStack, { children: [/* @__PURE__ */ jsx("code", { children: resolved.url }), /* @__PURE__ */ jsx(Badge, {
57
- tone: "success",
58
- children: SOURCE_LABELS[resolved.source]
59
- })] });
62
+ return /* @__PURE__ */ jsxs(HStack, { children: [
63
+ /* @__PURE__ */ jsx("code", { children: resolved.url }),
64
+ /* @__PURE__ */ jsx(Badge, {
65
+ tone: "success",
66
+ children: SOURCE_LABELS[resolved.source]
67
+ }),
68
+ resolved.source === "admin" ? null : /* @__PURE__ */ jsx(Meta, { children: "Nothing to confirm." })
69
+ ] });
60
70
  }
61
71
  /** Confirm the detected origin, replace it, or drop it. */
62
72
  function StoredForm(props) {
@@ -117,7 +127,6 @@ function EnterForm() {
117
127
  ]
118
128
  });
119
129
  }
120
- //#endregion
121
130
  export { PublicOriginCard };
122
131
 
123
132
  //# sourceMappingURL=admin-system-origin.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"admin-system-origin.mjs","names":[],"sources":["../../src/pages/admin-system-origin.tsx"],"sourcesContent":["import type { ResolvedPublicOrigin, StoredPublicOrigin } from \"@storyshelf/core/models\";\nimport type { HtmlEscapedString } from \"hono/utils/html\";\nimport {\n Alert,\n Badge,\n Button,\n Card,\n Field,\n FormActions,\n HStack,\n Meta,\n SectionTitle,\n} from \"../ui/components.tsx\";\nimport { csrfField } from \"../ui/csrf-field.tsx\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\n/** What the System page knows about the public origin. */\nexport interface PublicOriginView {\n /** What background jobs use now (configured, platform or confirmed); `null` when nothing is confirmed. */\n resolved: ResolvedPublicOrigin | null;\n /** The origin an admin's browser reported; `null` when none did. */\n stored: StoredPublicOrigin | null;\n /** Whether this deployment can learn an origin from an admin's browser (authentication is on). */\n learns: boolean;\n}\n\nconst SOURCE_LABELS = {\n config: \"configured (PUBLIC_BASE_URL)\",\n platform: \"detected from the hosting platform\",\n admin: \"confirmed by an admin\",\n} as const;\n\n/** Where absolute links in emails, commit statuses and signed Storybook URLs point (ADR 0007). */\nexport function PublicOriginCard(props: {\n origin: PublicOriginView;\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { resolved, stored, learns } = props.origin;\n // A configured or platform origin outranks what a browser reports, so there is nothing to learn.\n const reports = learns && (resolved === null || resolved.source === \"admin\");\n return (\n <Card>\n <SectionTitle>Public origin</SectionTitle>\n <Meta>\n The address links in emails, commit statuses and signed Storybook URLs use. It comes from{\" \"}\n <code>PUBLIC_BASE_URL</code>, then the hosting platform, then an address an admin confirmed\n here. An address detected from a browser is used for none of these until it is confirmed;\n sign-in (OAuth redirects) may use the address of the request itself.\n </Meta>\n {resolved === null ? (\n <NothingResolved learns={learns} stored={stored} />\n ) : (\n <Resolved resolved={resolved} />\n )}\n {reports ? <div data-report-origin=\"/admin/public-origin/report\" /> : null}\n {learns && stored !== null ? <StoredForm stored={stored} /> : null}\n {learns && stored === null ? <EnterForm /> : null}\n </Card>\n );\n}\n\nfunction NothingResolved(props: {\n learns: boolean;\n stored: StoredPublicOrigin | null;\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n if (props.stored !== null && !props.stored.confirmed) {\n return (\n <Alert tone=\"warning\" title=\"Public origin detected, not confirmed\">\n <code>{props.stored.origin}</code> was detected from your browser. Confirm it to enable\n notifications and signed Storybook URLs: until then no commit status or email is sent, and\n browsers that reach the Storybook by URL stay unavailable.\n </Alert>\n );\n }\n return (\n <Alert tone=\"warning\" title=\"No public origin is known\">\n Notifications are paused, and browsers that reach the Storybook by URL stay unavailable.{\" \"}\n {props.learns\n ? \"Reload this page: it reports the address you opened it with, for you to confirm.\"\n : \"Set PUBLIC_BASE_URL, or turn authentication on so an admin's browser can report it.\"}\n </Alert>\n );\n}\n\nfunction Resolved(props: {\n resolved: ResolvedPublicOrigin;\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { resolved } = props;\n return (\n <HStack>\n <code>{resolved.url}</code>\n <Badge tone=\"success\">{SOURCE_LABELS[resolved.source]}</Badge>\n </HStack>\n );\n}\n\n/** Confirm the detected origin, replace it, or drop it. */\nfunction StoredForm(props: {\n stored: StoredPublicOrigin;\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { stored } = props;\n return (\n <>\n <form\n method=\"post\"\n action=\"/admin/public-origin\"\n hx-post=\"/admin/public-origin\"\n hx-target=\"body\"\n >\n {csrfField()}\n <Field\n label=\"Stored origin\"\n name=\"origin\"\n value={stored.origin}\n hint={\n stored.confirmed\n ? \"Confirmed. Change it if this is not the address people use.\"\n : \"Detected from your browser. Confirm it, or enter the address people use.\"\n }\n />\n <FormActions>\n <Button type=\"submit\" variant=\"secondary\" size=\"sm\">\n {stored.confirmed ? \"Save\" : \"Confirm\"}\n </Button>\n </FormActions>\n </form>\n <form\n method=\"post\"\n action=\"/admin/public-origin/clear\"\n hx-post=\"/admin/public-origin/clear\"\n hx-target=\"body\"\n >\n {csrfField()}\n <Button type=\"submit\" variant=\"secondary\" size=\"sm\">\n Forget stored origin\n </Button>\n </form>\n </>\n );\n}\n\n/** Enter an origin by hand when none was reported. */\nfunction EnterForm(): HtmlEscapedString | Promise<HtmlEscapedString> {\n return (\n <form\n method=\"post\"\n action=\"/admin/public-origin\"\n hx-post=\"/admin/public-origin\"\n hx-target=\"body\"\n >\n {csrfField()}\n <Field label=\"Origin\" name=\"origin\" placeholder=\"https://shelf.example.com\" />\n <FormActions>\n <Button type=\"submit\" variant=\"secondary\" size=\"sm\">\n Save\n </Button>\n </FormActions>\n </form>\n );\n}\n"],"mappings":";;;;;;;;;;;AA2BA,MAAM,gBAAgB;CACpB,QAAQ;CACR,UAAU;CACV,OAAO;AACT;;AAGA,SAAgB,iBAAiB,OAEkB;CACjD,MAAM,EAAE,UAAU,QAAQ,WAAW,MAAM;CAE3C,MAAM,UAAU,WAAW,aAAa,QAAQ,SAAS,WAAW;CACpE,OACE,qBAAC,MAAD,EAAA,UAAA;EACE,oBAAC,cAAD,EAAA,UAAc,gBAA2B,CAAA;EACzC,qBAAC,MAAD,EAAA,UAAA;GAAM;GACsF;GAC1F,oBAAC,QAAD,EAAA,UAAM,kBAAqB,CAAA;GAAC;EAGxB,EAAA,CAAA;EACL,aAAa,OACZ,oBAAC,iBAAD;GAAyB;GAAgB;EAAS,CAAA,IAElD,oBAAC,UAAD,EAAoB,SAAW,CAAA;EAEhC,UAAU,oBAAC,OAAD,EAAK,sBAAmB,8BAA+B,CAAA,IAAI;EACrE,UAAU,WAAW,OAAO,oBAAC,YAAD,EAAoB,OAAS,CAAA,IAAI;EAC7D,UAAU,WAAW,OAAO,oBAAC,WAAD,CAAY,CAAA,IAAI;CACzC,EAAA,CAAA;AAEV;AAEA,SAAS,gBAAgB,OAG0B;CACjD,IAAI,MAAM,WAAW,QAAQ,CAAC,MAAM,OAAO,WACzC,OACE,qBAAC,OAAD;EAAO,MAAK;EAAU,OAAM;EAA5B,UAAA,CACE,oBAAC,QAAD,EAAA,UAAO,MAAM,OAAO,OAAa,CAAA,GAAC,6MAG7B;;CAGX,OACE,qBAAC,OAAD;EAAO,MAAK;EAAU,OAAM;EAA5B,UAAA;GAAwD;GACmC;GACxF,MAAM,SACH,qFACA;EACC;;AAEX;AAEA,SAAS,SAAS,OAEiC;CACjD,MAAM,EAAE,aAAa;CACrB,OACE,qBAAC,QAAD,EAAA,UAAA,CACE,oBAAC,QAAD,EAAA,UAAO,SAAS,IAAU,CAAA,GAC1B,oBAAC,OAAD;EAAO,MAAK;EAAW,UAAA,cAAc,SAAS;CAAe,CAAA,CACvD,EAAA,CAAA;AAEZ;;AAGA,SAAS,WAAW,OAE+B;CACjD,MAAM,EAAE,WAAW;CACnB,OACE,qBAAA,UAAA,EAAA,UAAA,CACE,qBAAC,QAAD;EACE,QAAO;EACP,QAAO;EACP,WAAQ;EACR,aAAU;EAJZ,UAAA;GAMG,UAAU;GACX,oBAAC,OAAD;IACE,OAAM;IACN,MAAK;IACL,OAAO,OAAO;IACd,MACE,OAAO,YACH,gEACA;GAEP,CAAA;GACD,oBAAC,aAAD,EAAA,UACE,oBAAC,QAAD;IAAQ,MAAK;IAAS,SAAQ;IAAY,MAAK;IAC5C,UAAA,OAAO,YAAY,SAAS;GACvB,CAAA,EACG,CAAA;EACT;CACN,CAAA,GAAA,qBAAC,QAAD;EACE,QAAO;EACP,QAAO;EACP,WAAQ;EACR,aAAU;EAJZ,UAAA,CAMG,UAAU,GACX,oBAAC,QAAD;GAAQ,MAAK;GAAS,SAAQ;GAAY,MAAK;GAAK,UAAA;EAE5C,CAAA,CACJ;CACN,CAAA,CAAA,EAAA,CAAA;AAEN;;AAGA,SAAS,YAA4D;CACnE,OACE,qBAAC,QAAD;EACE,QAAO;EACP,QAAO;EACP,WAAQ;EACR,aAAU;EAJZ,UAAA;GAMG,UAAU;GACX,oBAAC,OAAD;IAAO,OAAM;IAAS,MAAK;IAAS,aAAY;GAA6B,CAAA;GAC7E,oBAAC,aAAD,EAAA,UACE,oBAAC,QAAD;IAAQ,MAAK;IAAS,SAAQ;IAAY,MAAK;IAAK,UAAA;GAE5C,CAAA,EACG,CAAA;EACT;;AAEV"}
1
+ {"version":3,"file":"admin-system-origin.mjs","names":[],"sources":["../../src/pages/admin-system-origin.tsx"],"sourcesContent":["import type { ResolvedPublicOrigin, StoredPublicOrigin } from \"@storyshelf/core/models\";\nimport type { HtmlEscapedString } from \"hono/utils/html\";\nimport {\n Alert,\n Badge,\n Button,\n Card,\n Field,\n FormActions,\n HStack,\n Meta,\n SectionTitle,\n} from \"../ui/components.tsx\";\nimport { csrfField } from \"../ui/csrf-field.tsx\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\n/** What the System page knows about the public origin. */\nexport interface PublicOriginView {\n /** What background jobs use now (configured, platform or confirmed); `null` when nothing is confirmed. */\n resolved: ResolvedPublicOrigin | null;\n /** The origin an admin's browser reported; `null` when none did. */\n stored: StoredPublicOrigin | null;\n /** Whether this deployment can learn an origin from an admin's browser (authentication is on). */\n learns: boolean;\n}\n\nconst SOURCE_LABELS = {\n config: \"configured (PUBLIC_BASE_URL)\",\n platform: \"detected from the hosting platform\",\n admin: \"confirmed by an admin\",\n} as const;\n\n/** Where absolute links in emails, commit statuses and signed Storybook URLs point (ADR 0007). */\nexport function PublicOriginCard(props: {\n origin: PublicOriginView;\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { resolved, stored, learns } = props.origin;\n // A configured or platform origin outranks what a browser reports: there is nothing to learn or\n // to confirm, so neither the beacon nor the forms are offered (#315).\n const fixed = resolved !== null && resolved.source !== \"admin\";\n const reports = learns && !fixed;\n return (\n <Card>\n <SectionTitle>Public origin</SectionTitle>\n <Meta>\n The address links in emails, commit statuses and signed Storybook URLs use. It comes from{\" \"}\n <code>PUBLIC_BASE_URL</code>, then the hosting platform, then an address an admin confirmed\n here. An address detected from a browser is used for none of these until it is confirmed;\n sign-in (OAuth redirects) may use the address of the request itself.\n </Meta>\n {resolved === null ? (\n <NothingResolved learns={learns} stored={stored} />\n ) : (\n <Resolved resolved={resolved} />\n )}\n {reports ? <div data-report-origin=\"/admin/public-origin/report\" /> : null}\n {fixed ? null : <OriginForms learns={learns} stored={stored} />}\n </Card>\n );\n}\n\nfunction OriginForms(props: {\n learns: boolean;\n stored: StoredPublicOrigin | null;\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n if (!props.learns) {\n return <></>;\n }\n return props.stored === null ? <EnterForm /> : <StoredForm stored={props.stored} />;\n}\n\nfunction NothingResolved(props: {\n learns: boolean;\n stored: StoredPublicOrigin | null;\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n if (props.stored !== null && !props.stored.confirmed) {\n return (\n <Alert tone=\"warning\" title=\"Public origin detected, not confirmed\">\n <code>{props.stored.origin}</code> was detected from your browser. Confirm it to enable\n notifications and signed Storybook URLs: until then no commit status or email is sent, and\n browsers that reach the Storybook by URL stay unavailable.\n </Alert>\n );\n }\n return (\n <Alert tone=\"warning\" title=\"No public origin is known\">\n Notifications are paused, and browsers that reach the Storybook by URL stay unavailable.{\" \"}\n {props.learns\n ? \"Reload this page: it reports the address you opened it with, for you to confirm.\"\n : \"Set PUBLIC_BASE_URL, or turn authentication on so an admin's browser can report it.\"}\n </Alert>\n );\n}\n\nfunction Resolved(props: {\n resolved: ResolvedPublicOrigin;\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { resolved } = props;\n return (\n <HStack>\n <code>{resolved.url}</code>\n <Badge tone=\"success\">{SOURCE_LABELS[resolved.source]}</Badge>\n {resolved.source === \"admin\" ? null : <Meta>Nothing to confirm.</Meta>}\n </HStack>\n );\n}\n\n/** Confirm the detected origin, replace it, or drop it. */\nfunction StoredForm(props: {\n stored: StoredPublicOrigin;\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { stored } = props;\n return (\n <>\n <form\n method=\"post\"\n action=\"/admin/public-origin\"\n hx-post=\"/admin/public-origin\"\n hx-target=\"body\"\n >\n {csrfField()}\n <Field\n label=\"Stored origin\"\n name=\"origin\"\n value={stored.origin}\n hint={\n stored.confirmed\n ? \"Confirmed. Change it if this is not the address people use.\"\n : \"Detected from your browser. Confirm it, or enter the address people use.\"\n }\n />\n <FormActions>\n <Button type=\"submit\" variant=\"secondary\" size=\"sm\">\n {stored.confirmed ? \"Save\" : \"Confirm\"}\n </Button>\n </FormActions>\n </form>\n <form\n method=\"post\"\n action=\"/admin/public-origin/clear\"\n hx-post=\"/admin/public-origin/clear\"\n hx-target=\"body\"\n >\n {csrfField()}\n <Button type=\"submit\" variant=\"secondary\" size=\"sm\">\n Forget stored origin\n </Button>\n </form>\n </>\n );\n}\n\n/** Enter an origin by hand when none was reported. */\nfunction EnterForm(): HtmlEscapedString | Promise<HtmlEscapedString> {\n return (\n <form\n method=\"post\"\n action=\"/admin/public-origin\"\n hx-post=\"/admin/public-origin\"\n hx-target=\"body\"\n >\n {csrfField()}\n <Field label=\"Origin\" name=\"origin\" placeholder=\"https://shelf.example.com\" />\n <FormActions>\n <Button type=\"submit\" variant=\"secondary\" size=\"sm\">\n Save\n </Button>\n </FormActions>\n </form>\n );\n}\n"],"mappings":"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"}
@@ -7,7 +7,7 @@ import { Table } from "../ui/table.mjs";
7
7
  import { targetLabel } from "../targets.mjs";
8
8
  import { jsx, jsxs } from "hono/jsx/jsx-runtime";
9
9
  import { unavailableReason } from "@storyshelf/core/adapter/capture-runner";
10
- //#region src/pages/admin-system-targets.tsx
10
+ // src/pages/admin-system-targets.tsx
11
11
  /** The browsers the capture runner offers and whether each can render now (ADR 0007, decision 7). */
12
12
  function CaptureTargetsCard(props) {
13
13
  const { targets } = props;
@@ -47,7 +47,6 @@ function CaptureTargetsCard(props) {
47
47
  }) })] }) })
48
48
  ] });
49
49
  }
50
- //#endregion
51
50
  export { CaptureTargetsCard };
52
51
 
53
52
  //# sourceMappingURL=admin-system-targets.mjs.map
@@ -12,7 +12,7 @@ import { PublicOriginCard } from "./admin-system-origin.mjs";
12
12
  import { CaptureTargetsCard } from "./admin-system-targets.mjs";
13
13
  import { NotificationsPausedNotice } from "./notifications-paused.mjs";
14
14
  import { Fragment, jsx, jsxs } from "hono/jsx/jsx-runtime";
15
- //#region src/pages/admin-system.tsx
15
+ // src/pages/admin-system.tsx
16
16
  function healthTone(state) {
17
17
  if (state === "ok") return "success";
18
18
  if (state === "starting") return "info";
@@ -287,7 +287,6 @@ function renderAdminSystemPage(data) {
287
287
  ]
288
288
  });
289
289
  }
290
- //#endregion
291
290
  export { renderAdminSystemPage };
292
291
 
293
292
  //# sourceMappingURL=admin-system.mjs.map
@@ -26,7 +26,7 @@ import { BuildModel, CaptureAttemptModel, CaptureLogModel, LabelModel, ProjectMo
26
26
  import { CANCELLABLE_BUILD_STATUSES } from "@storyshelf/core/types";
27
27
  import { Fragment, jsx, jsxs } from "hono/jsx/jsx-runtime";
28
28
  import { createUrlBuilder } from "@storyshelf/core/urls";
29
- //#region src/pages/build-detail.tsx
29
+ // src/pages/build-detail.tsx
30
30
  /** Map a capture log level to its badge tone. */
31
31
  function logTone(level) {
32
32
  if (level === "error") return "danger";
@@ -414,7 +414,6 @@ async function renderBuildDetailPage(buildId, usageByStory = /* @__PURE__ */ new
414
414
  ]
415
415
  });
416
416
  }
417
- //#endregion
418
417
  export { renderBuildDetailPage };
419
418
 
420
419
  //# sourceMappingURL=build-detail.mjs.map
@@ -7,7 +7,7 @@ import { Kbd } from "../ui/kbd.mjs";
7
7
  import { groupOf, neighbours, snapshotPageUrl } from "./build-diff-model.mjs";
8
8
  import { actionBar, actionGroup, actionHints, actionPosition } from "../ui/styles/review-layout.mjs";
9
9
  import { jsx, jsxs } from "hono/jsx/jsx-runtime";
10
- //#region src/pages/build-diff-actions.tsx
10
+ // src/pages/build-diff-actions.tsx
11
11
  function apiBase(project, build) {
12
12
  return `/api/v1/projects/${project.slug}/builds/${build.id}`;
13
13
  }
@@ -210,7 +210,6 @@ function DiffActionBar(props) {
210
210
  ]
211
211
  });
212
212
  }
213
- //#endregion
214
213
  export { DiffActionBar };
215
214
 
216
215
  //# sourceMappingURL=build-diff-actions.mjs.map
@@ -9,7 +9,7 @@ import { VStack } from "../ui/stacks.mjs";
9
9
  import { csrfField } from "../ui/csrf-field.mjs";
10
10
  import { reviewComment, reviewCommentActions, reviewCommentBody, reviewCommentHead } from "../ui/styles/review.mjs";
11
11
  import { jsx, jsxs } from "hono/jsx/jsx-runtime";
12
- //#region src/pages/build-diff-comments.tsx
12
+ // src/pages/build-diff-comments.tsx
13
13
  /** One comment with an optional resolve action. */
14
14
  function CommentCard(props) {
15
15
  const { project, build, comment, canReview, anchor } = props;
@@ -100,7 +100,6 @@ function DiffComments(props) {
100
100
  })
101
101
  ] });
102
102
  }
103
- //#endregion
104
103
  export { CommentCard, DiffComments };
105
104
 
106
105
  //# sourceMappingURL=build-diff-comments.mjs.map
@@ -0,0 +1,53 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { css } from "../ui/css.mjs";
5
+ import { HStack } from "../ui/stacks.mjs";
6
+ import { snapshotImageUrl } from "./build-diff-model.mjs";
7
+ import { jsx, jsxs } from "hono/jsx/jsx-runtime";
8
+ // src/pages/build-diff-default.tsx
9
+ const comparisonImage = css`
10
+ /* default-comparison-image */
11
+ max-width: 100%;
12
+ `;
13
+ function DefaultComparisonView(props) {
14
+ const { project, build, snapshot, comparison } = props;
15
+ const params = new URLSearchParams({
16
+ reference: "default",
17
+ id: comparison.id,
18
+ version: comparison.version
19
+ });
20
+ const baseline = `${snapshotImageUrl(project, build, snapshot, "baseline")}?${params}`;
21
+ return /* @__PURE__ */ jsxs("details", { children: [
22
+ /* @__PURE__ */ jsxs("summary", { children: [
23
+ "Compare with current default branch (",
24
+ comparison.branch,
25
+ ")"
26
+ ] }),
27
+ /* @__PURE__ */ jsxs("p", { children: [
28
+ "This is a secondary comparison. Approval still uses the primary comparison above. Default baseline updated ",
29
+ comparison.version,
30
+ "."
31
+ ] }),
32
+ /* @__PURE__ */ jsx("p", { children: comparison.environment === "different" ? "Recorded environment fingerprints differ; some components may be unknown and differences may be environmental." : comparison.environment === "unknown" ? "Capture environment information is unavailable." : "Recorded capture environment fingerprints match; some components may be unknown." }),
33
+ comparison.available ? /* @__PURE__ */ jsxs(HStack, {
34
+ align: "start",
35
+ children: [/* @__PURE__ */ jsx(BaselineImage, { src: baseline }), /* @__PURE__ */ jsxs("figure", { children: [/* @__PURE__ */ jsx("figcaption", { children: "This build" }), /* @__PURE__ */ jsx("img", {
36
+ src: snapshotImageUrl(project, build, snapshot, "image"),
37
+ alt: "This build's screenshot",
38
+ class: comparisonImage
39
+ })] })]
40
+ }) : /* @__PURE__ */ jsx("p", { children: "The current default-branch baseline image is unavailable." })
41
+ ] });
42
+ }
43
+ function BaselineImage(props) {
44
+ return /* @__PURE__ */ jsxs("figure", { children: [/* @__PURE__ */ jsx("figcaption", { children: "Current default-branch baseline" }), /* @__PURE__ */ jsx("img", {
45
+ src: props.src,
46
+ alt: "Current default-branch baseline",
47
+ loading: "lazy",
48
+ class: comparisonImage
49
+ })] });
50
+ }
51
+ export { DefaultComparisonView };
52
+
53
+ //# sourceMappingURL=build-diff-default.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build-diff-default.mjs","names":[],"sources":["../../src/pages/build-diff-default.tsx"],"sourcesContent":["import type { Build, Project, Snapshot } from \"@storyshelf/core/schema\";\nimport type { HtmlEscapedString } from \"hono/utils/html\";\nimport type { DefaultComparison } from \"../routers/baseline-reference.ts\";\nimport { HStack } from \"../ui/components.tsx\";\nimport { css } from \"../ui/css.ts\";\nimport { snapshotImageUrl } from \"./build-diff-model.ts\";\n\nconst comparisonImage = css`\n /* default-comparison-image */\n max-width: 100%;\n`;\n\n/* eslint-disable promise-function-async -- Hono JSX components may return HTML or a promise */\nexport function DefaultComparisonView(props: {\n project: Project;\n build: Build;\n snapshot: Snapshot;\n comparison: DefaultComparison;\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { project, build, snapshot, comparison } = props;\n const params = new URLSearchParams({\n reference: \"default\",\n id: comparison.id,\n version: comparison.version,\n });\n const baseline = `${snapshotImageUrl(project, build, snapshot, \"baseline\")}?${params}`;\n return (\n <details>\n <summary>Compare with current default branch ({comparison.branch})</summary>\n <p>\n This is a secondary comparison. Approval still uses the primary comparison above. Default\n baseline updated {comparison.version}.\n </p>\n <p>\n {comparison.environment === \"different\"\n ? \"Recorded environment fingerprints differ; some components may be unknown and differences may be environmental.\"\n : comparison.environment === \"unknown\"\n ? \"Capture environment information is unavailable.\"\n : \"Recorded capture environment fingerprints match; some components may be unknown.\"}\n </p>\n {comparison.available ? (\n <HStack align=\"start\">\n <BaselineImage src={baseline} />\n <figure>\n <figcaption>This build</figcaption>\n <img\n src={snapshotImageUrl(project, build, snapshot, \"image\")}\n alt=\"This build's screenshot\"\n class={comparisonImage}\n />\n </figure>\n </HStack>\n ) : (\n <p>The current default-branch baseline image is unavailable.</p>\n )}\n </details>\n );\n}\n\nfunction BaselineImage(props: { src: string }): HtmlEscapedString | Promise<HtmlEscapedString> {\n return (\n <figure>\n <figcaption>Current default-branch baseline</figcaption>\n <img\n src={props.src}\n alt=\"Current default-branch baseline\"\n loading=\"lazy\"\n class={comparisonImage}\n />\n </figure>\n );\n}\n"],"mappings":";;;;;;;;AAOA,MAAM,kBAAkB,GAAG;;;;AAM3B,SAAgB,sBAAsB,OAKa;CACjD,MAAM,EAAE,SAAS,OAAO,UAAU,eAAe;CACjD,MAAM,SAAS,IAAI,gBAAgB;EACjC,WAAW;EACX,IAAI,WAAW;EACf,SAAS,WAAW;CACtB,CAAC;CACD,MAAM,WAAW,GAAG,iBAAiB,SAAS,OAAO,UAAU,UAAU,EAAE,GAAG;CAC9E,OACE,qBAAC,WAAD,EAAA,UAAA;EACE,qBAAC,WAAD,EAAA,UAAA;GAAS;GAAsC,WAAW;GAAO;EAAU,EAAA,CAAA;EAC3E,qBAAC,KAAD,EAAA,UAAA;GAAG;GAEiB,WAAW;GAAQ;EACpC,EAAA,CAAA;EACH,oBAAC,KAAD,EAAA,UACG,WAAW,gBAAgB,cACxB,mHACA,WAAW,gBAAgB,YACzB,oDACA,mFACL,CAAA;EACF,WAAW,YACV,qBAAC,QAAD;GAAQ,OAAM;GAAd,UAAA,CACE,oBAAC,eAAD,EAAe,KAAK,SAAW,CAAA,GAC/B,qBAAC,UAAD,EAAA,UAAA,CACE,oBAAC,cAAD,EAAA,UAAY,aAAsB,CAAA,GAClC,oBAAC,OAAD;IACE,KAAK,iBAAiB,SAAS,OAAO,UAAU,OAAO;IACvD,KAAI;IACJ,OAAO;GACR,CAAA,CACK,EAAA,CAAA,CACF;EAER,CAAA,IAAA,oBAAC,KAAD,EAAA,UAAG,4DAA4D,CAAA;CAE1D,EAAA,CAAA;AAEb;AAEA,SAAS,cAAc,OAAwE;CAC7F,OACE,qBAAC,UAAD,EAAA,UAAA,CACE,oBAAC,cAAD,EAAA,UAAY,kCAA2C,CAAA,GACvD,oBAAC,OAAD;EACE,KAAK,MAAM;EACX,KAAI;EACJ,SAAQ;EACR,OAAO;CACR,CAAA,CACK,EAAA,CAAA;AAEZ"}
@@ -5,7 +5,7 @@ import { Alert } from "../ui/feedback.mjs";
5
5
  import { StaleBaselineNotice } from "./build-diff-stale.mjs";
6
6
  import { environmentChangeOf, labelOf, summariseEnvironmentChanges } from "./environment-change.mjs";
7
7
  import { Fragment, jsx, jsxs } from "hono/jsx/jsx-runtime";
8
- //#region src/pages/build-diff-environment.tsx
8
+ // src/pages/build-diff-environment.tsx
9
9
  /**
10
10
  * Build-level note: some snapshots were captured in a different environment than their baseline.
11
11
  * The default branch migrates them without a diff; a feature branch still diffs them (ADR 0005).
@@ -76,7 +76,6 @@ function ReviewNotices(props) {
76
76
  migrated: build.isDefault
77
77
  })] });
78
78
  }
79
- //#endregion
80
79
  export { EnvironmentChangeNotice, EnvironmentChangeSnapshotNotice, ReviewNotices };
81
80
 
82
81
  //# sourceMappingURL=build-diff-environment.mjs.map
@@ -9,7 +9,7 @@ import { a11ySummaryOf, a11yUnavailableNote } from "./a11y-violations.mjs";
9
9
  import { reviewProgress } from "./build-diff-model.mjs";
10
10
  import { headerActions, headerMain, headerMeta, headerProgress, headerSide, headerTitle, reviewHeader } from "../ui/styles/review-layout.mjs";
11
11
  import { Fragment, jsx, jsxs } from "hono/jsx/jsx-runtime";
12
- //#region src/pages/build-diff-header.tsx
12
+ // src/pages/build-diff-header.tsx
13
13
  /** Approve-all / reject-all bulk actions (each asks for confirmation). */
14
14
  function BulkActions({ project, build, pendingCount }) {
15
15
  const base = `/api/v1/projects/${project.slug}/builds/${build.id}`;
@@ -129,7 +129,6 @@ function DiffHeader(props) {
129
129
  })]
130
130
  });
131
131
  }
132
- //#endregion
133
132
  export { DiffHeader };
134
133
 
135
134
  //# sourceMappingURL=build-diff-header.mjs.map