@storyshelf/app 0.6.2 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -4
- package/dist/app-types.d.mts +2 -6
- package/dist/asset-manifest.mjs +14 -7
- package/dist/asset-manifest.mjs.map +1 -1
- package/dist/assets/{htmx.min.js → htmx.mjs} +9 -1
- package/dist/assets/htmx.mjs.map +1 -0
- package/dist/assets/mark.mjs +9 -0
- package/dist/assets/mark.mjs.map +1 -0
- package/dist/boot-tasks.mjs +79 -0
- package/dist/boot-tasks.mjs.map +1 -0
- package/dist/build-state-flags.mjs +17 -0
- package/dist/build-state-flags.mjs.map +1 -0
- package/dist/build-state-setup.mjs +74 -0
- package/dist/build-state-setup.mjs.map +1 -0
- package/dist/capture-alerts.mjs +45 -0
- package/dist/capture-alerts.mjs.map +1 -0
- package/dist/capture-process.mjs +58 -0
- package/dist/capture-process.mjs.map +1 -0
- package/dist/capture-setup.mjs +96 -119
- package/dist/capture-setup.mjs.map +1 -1
- package/dist/connections.mjs +33 -0
- package/dist/connections.mjs.map +1 -0
- package/dist/consumer-setup.mjs +57 -0
- package/dist/consumer-setup.mjs.map +1 -0
- package/dist/credential-keys.mjs +1 -2
- package/dist/credentials.mjs +1 -2
- package/dist/git-status.mjs +45 -0
- package/dist/git-status.mjs.map +1 -0
- package/dist/hidden-data.mjs +23 -0
- package/dist/hidden-data.mjs.map +1 -0
- package/dist/index.d.mts +3 -5
- package/dist/index.mjs +204 -127
- package/dist/index.mjs.map +1 -1
- package/dist/insights/budget.mjs +48 -0
- package/dist/insights/budget.mjs.map +1 -0
- package/dist/insights/deps.mjs +20 -0
- package/dist/insights/deps.mjs.map +1 -0
- package/dist/insights/errors.mjs +21 -0
- package/dist/insights/errors.mjs.map +1 -0
- package/dist/insights/health-panel.mjs +39 -0
- package/dist/insights/health-panel.mjs.map +1 -0
- package/dist/insights/job.mjs +102 -0
- package/dist/insights/job.mjs.map +1 -0
- package/dist/insights/load-health.mjs +58 -0
- package/dist/insights/load-health.mjs.map +1 -0
- package/dist/insights/load-triage.mjs +97 -0
- package/dist/insights/load-triage.mjs.map +1 -0
- package/dist/insights/panel.mjs +27 -0
- package/dist/insights/panel.mjs.map +1 -0
- package/dist/insights/request.mjs +112 -0
- package/dist/insights/request.mjs.map +1 -0
- package/dist/insights/stale.mjs +20 -0
- package/dist/insights/stale.mjs.map +1 -0
- package/dist/insights/usage-report.mjs +59 -0
- package/dist/insights/usage-report.mjs.map +1 -0
- package/dist/insights/view.mjs +47 -0
- package/dist/insights/view.mjs.map +1 -0
- package/dist/job-canceller.mjs +13 -0
- package/dist/job-canceller.mjs.map +1 -0
- package/dist/job-queue.mjs +50 -0
- package/dist/job-queue.mjs.map +1 -0
- package/dist/jobs-setup.mjs +131 -0
- package/dist/jobs-setup.mjs.map +1 -0
- package/dist/lifecycle.mjs +93 -22
- package/dist/lifecycle.mjs.map +1 -1
- package/dist/middleware/auth-gate.mjs +5 -4
- package/dist/middleware/auth-gate.mjs.map +1 -1
- package/dist/middleware/csrf.mjs +1 -2
- package/dist/middleware/rate-limit.mjs +42 -35
- package/dist/middleware/rate-limit.mjs.map +1 -1
- package/dist/middleware/request-log.mjs +4 -4
- package/dist/middleware/request-log.mjs.map +1 -1
- package/dist/middleware/require-review.mjs +50 -0
- package/dist/middleware/require-review.mjs.map +1 -0
- package/dist/middleware/require-testing.mjs +40 -0
- package/dist/middleware/require-testing.mjs.map +1 -0
- package/dist/middleware/server-timing.mjs +1 -2
- package/dist/middleware/setup-gate.mjs +1 -2
- package/dist/middleware/setup-gate.mjs.map +1 -1
- package/dist/middleware/store-scope.mjs +13 -4
- package/dist/middleware/store-scope.mjs.map +1 -1
- package/dist/notifications-pause.mjs +32 -0
- package/dist/notifications-pause.mjs.map +1 -0
- package/dist/notify.d.mts +3 -4
- package/dist/notify.mjs +79 -41
- package/dist/notify.mjs.map +1 -1
- package/dist/pages/a11y-violations.mjs +115 -0
- package/dist/pages/a11y-violations.mjs.map +1 -0
- package/dist/pages/admin-ai.mjs +77 -0
- package/dist/pages/admin-ai.mjs.map +1 -0
- package/dist/pages/admin-system-origin.mjs +132 -0
- package/dist/pages/admin-system-origin.mjs.map +1 -0
- package/dist/pages/admin-system-targets.mjs +52 -0
- package/dist/pages/admin-system-targets.mjs.map +1 -0
- package/dist/pages/admin-system.mjs +64 -15
- package/dist/pages/admin-system.mjs.map +1 -1
- package/dist/pages/build-detail.mjs +105 -78
- package/dist/pages/build-detail.mjs.map +1 -1
- package/dist/pages/build-diff-actions.mjs +88 -35
- package/dist/pages/build-diff-actions.mjs.map +1 -1
- package/dist/pages/build-diff-comments.mjs +17 -16
- package/dist/pages/build-diff-comments.mjs.map +1 -1
- package/dist/pages/build-diff-default.mjs +53 -0
- package/dist/pages/build-diff-default.mjs.map +1 -0
- package/dist/pages/build-diff-environment.mjs +81 -0
- package/dist/pages/build-diff-environment.mjs.map +1 -0
- package/dist/pages/build-diff-header.mjs +23 -6
- package/dist/pages/build-diff-header.mjs.map +1 -1
- package/dist/pages/build-diff-insight.mjs +112 -0
- package/dist/pages/build-diff-insight.mjs.map +1 -0
- package/dist/pages/build-diff-model.mjs +74 -12
- package/dist/pages/build-diff-model.mjs.map +1 -1
- package/dist/pages/build-diff-nav.mjs +29 -14
- package/dist/pages/build-diff-nav.mjs.map +1 -1
- package/dist/pages/build-diff-reference.mjs +84 -0
- package/dist/pages/build-diff-reference.mjs.map +1 -0
- package/dist/pages/build-diff-shortcuts.mjs +6 -3
- package/dist/pages/build-diff-shortcuts.mjs.map +1 -1
- package/dist/pages/build-diff-stale.mjs +1 -2
- package/dist/pages/build-diff-usage.mjs +62 -0
- package/dist/pages/build-diff-usage.mjs.map +1 -0
- package/dist/pages/build-diff-viewer.mjs +97 -14
- package/dist/pages/build-diff-viewer.mjs.map +1 -1
- package/dist/pages/build-diff.mjs +37 -22
- package/dist/pages/build-diff.mjs.map +1 -1
- package/dist/pages/build-tests.mjs +73 -0
- package/dist/pages/build-tests.mjs.map +1 -0
- package/dist/pages/component-detail.mjs +248 -0
- package/dist/pages/component-detail.mjs.map +1 -0
- package/dist/pages/component-stories.mjs +167 -0
- package/dist/pages/component-stories.mjs.map +1 -0
- package/dist/pages/compute-jobs.mjs +14 -28
- package/dist/pages/compute-jobs.mjs.map +1 -1
- package/dist/pages/design-links.mjs +181 -0
- package/dist/pages/design-links.mjs.map +1 -0
- package/dist/pages/environment-change.mjs +52 -0
- package/dist/pages/environment-change.mjs.map +1 -0
- package/dist/pages/health-panel.mjs +125 -0
- package/dist/pages/health-panel.mjs.map +1 -0
- package/dist/pages/invite.mjs +1 -2
- package/dist/pages/label-detail.mjs +1 -2
- package/dist/pages/library-grid.mjs +104 -0
- package/dist/pages/library-grid.mjs.map +1 -0
- package/dist/pages/library-model.mjs +46 -0
- package/dist/pages/library-model.mjs.map +1 -0
- package/dist/pages/library-snapshots.mjs +129 -0
- package/dist/pages/library-snapshots.mjs.map +1 -0
- package/dist/pages/library.mjs +113 -220
- package/dist/pages/library.mjs.map +1 -1
- package/dist/pages/login.mjs +1 -2
- package/dist/pages/notifications-paused.mjs +21 -0
- package/dist/pages/notifications-paused.mjs.map +1 -0
- package/dist/pages/passkey-ceremony.mjs +1 -2
- package/dist/pages/profile.mjs +54 -50
- package/dist/pages/profile.mjs.map +1 -1
- package/dist/pages/project-builds-table.mjs +3 -4
- package/dist/pages/project-builds-table.mjs.map +1 -1
- package/dist/pages/project-builds.mjs +52 -23
- package/dist/pages/project-builds.mjs.map +1 -1
- package/dist/pages/project-create.mjs +1 -2
- package/dist/pages/project-settings.mjs +52 -14
- package/dist/pages/project-settings.mjs.map +1 -1
- package/dist/pages/projects-card.mjs +15 -3
- package/dist/pages/projects-card.mjs.map +1 -1
- package/dist/pages/projects.mjs +7 -4
- package/dist/pages/projects.mjs.map +1 -1
- package/dist/pages/review-changeset.mjs +115 -0
- package/dist/pages/review-changeset.mjs.map +1 -0
- package/dist/pages/review-comments.mjs +102 -0
- package/dist/pages/review-comments.mjs.map +1 -0
- package/dist/pages/review-verdict.mjs +101 -0
- package/dist/pages/review-verdict.mjs.map +1 -0
- package/dist/pages/root.mjs +1 -2
- package/dist/pages/settings-ai.mjs +68 -0
- package/dist/pages/settings-ai.mjs.map +1 -0
- package/dist/pages/settings-capture.mjs +123 -0
- package/dist/pages/settings-capture.mjs.map +1 -0
- package/dist/pages/settings-design-credential.mjs +98 -0
- package/dist/pages/settings-design-credential.mjs.map +1 -0
- package/dist/pages/settings-design-links.mjs +178 -0
- package/dist/pages/settings-design-links.mjs.map +1 -0
- package/dist/pages/settings-design.mjs +47 -0
- package/dist/pages/settings-design.mjs.map +1 -0
- package/dist/pages/settings-general.mjs +47 -23
- package/dist/pages/settings-general.mjs.map +1 -1
- package/dist/pages/settings-labels.mjs +44 -4
- package/dist/pages/settings-labels.mjs.map +1 -1
- package/dist/pages/settings-links.mjs +111 -0
- package/dist/pages/settings-links.mjs.map +1 -0
- package/dist/pages/settings-members.mjs +1 -2
- package/dist/pages/settings-notifications-git.mjs +127 -0
- package/dist/pages/settings-notifications-git.mjs.map +1 -0
- package/dist/pages/settings-notifications.mjs +80 -44
- package/dist/pages/settings-notifications.mjs.map +1 -1
- package/dist/pages/settings-tests.mjs +34 -4
- package/dist/pages/settings-tests.mjs.map +1 -1
- package/dist/pages/settings-tokens.mjs +1 -2
- package/dist/pages/settings-webhooks.mjs +1 -2
- package/dist/pages/storybook.mjs +1 -2
- package/dist/pages/target-choices.mjs +38 -0
- package/dist/pages/target-choices.mjs.map +1 -0
- package/dist/publish-setup.mjs +88 -0
- package/dist/publish-setup.mjs.map +1 -0
- package/dist/retention-timer.mjs +12 -12
- package/dist/retention-timer.mjs.map +1 -1
- package/dist/review-events.mjs +47 -0
- package/dist/review-events.mjs.map +1 -0
- package/dist/routers/admin-pages.mjs +46 -5
- package/dist/routers/admin-pages.mjs.map +1 -1
- package/dist/routers/admin-public-origin.mjs +92 -0
- package/dist/routers/admin-public-origin.mjs.map +1 -0
- package/dist/routers/admin.mjs +8 -4
- package/dist/routers/admin.mjs.map +1 -1
- package/dist/routers/assets.mjs +10 -4
- package/dist/routers/assets.mjs.map +1 -1
- package/dist/routers/attempts.mjs +1 -2
- package/dist/routers/auth-sync.mjs +1 -2
- package/dist/routers/auth.mjs +2 -3
- package/dist/routers/baseline-guard.mjs +4 -5
- package/dist/routers/baseline-guard.mjs.map +1 -1
- package/dist/routers/baseline-reference.mjs +72 -0
- package/dist/routers/baseline-reference.mjs.map +1 -0
- package/dist/routers/baselines.handlers.mjs +63 -0
- package/dist/routers/baselines.handlers.mjs.map +1 -0
- package/dist/routers/baselines.mjs +124 -0
- package/dist/routers/baselines.mjs.map +1 -0
- package/dist/routers/build-cancel.mjs +67 -0
- package/dist/routers/build-cancel.mjs.map +1 -0
- package/dist/routers/build-changeset.mjs +69 -0
- package/dist/routers/build-changeset.mjs.map +1 -0
- package/dist/routers/build-reviews.handlers.mjs +85 -0
- package/dist/routers/build-reviews.handlers.mjs.map +1 -0
- package/dist/routers/build-reviews.mjs +73 -0
- package/dist/routers/build-reviews.mjs.map +1 -0
- package/dist/routers/builds.handlers.mjs +65 -60
- package/dist/routers/builds.handlers.mjs.map +1 -1
- package/dist/routers/builds.mjs +77 -19
- package/dist/routers/builds.mjs.map +1 -1
- package/dist/routers/capture-cache.mjs +29 -0
- package/dist/routers/capture-cache.mjs.map +1 -0
- package/dist/routers/capture-config.mjs +34 -0
- package/dist/routers/capture-config.mjs.map +1 -0
- package/dist/routers/comments.mjs +42 -17
- package/dist/routers/comments.mjs.map +1 -1
- package/dist/routers/comparison-image.mjs +29 -0
- package/dist/routers/comparison-image.mjs.map +1 -0
- package/dist/routers/component-catalog.handlers.mjs +70 -0
- package/dist/routers/component-catalog.handlers.mjs.map +1 -0
- package/dist/routers/component.handlers.mjs +120 -0
- package/dist/routers/component.handlers.mjs.map +1 -0
- package/dist/routers/connections.mjs +117 -0
- package/dist/routers/connections.mjs.map +1 -0
- package/dist/routers/design-connect.handlers.mjs +43 -0
- package/dist/routers/design-connect.handlers.mjs.map +1 -0
- package/dist/routers/design.handlers.mjs +113 -0
- package/dist/routers/design.handlers.mjs.map +1 -0
- package/dist/routers/flash.mjs +1 -2
- package/dist/routers/health-report.mjs +3 -5
- package/dist/routers/health-report.mjs.map +1 -1
- package/dist/routers/health.mjs +5 -5
- package/dist/routers/health.mjs.map +1 -1
- package/dist/routers/helpers.mjs +20 -5
- package/dist/routers/helpers.mjs.map +1 -1
- package/dist/routers/htmx.mjs +1 -2
- package/dist/routers/insights-ui.mjs +63 -0
- package/dist/routers/insights-ui.mjs.map +1 -0
- package/dist/routers/insights.handlers.mjs +44 -0
- package/dist/routers/insights.handlers.mjs.map +1 -0
- package/dist/routers/insights.mjs +188 -0
- package/dist/routers/insights.mjs.map +1 -0
- package/dist/routers/labels.mjs +1 -2
- package/dist/routers/links.handlers.mjs +79 -0
- package/dist/routers/links.handlers.mjs.map +1 -0
- package/dist/routers/links.mjs +138 -0
- package/dist/routers/links.mjs.map +1 -0
- package/dist/routers/media.mjs +30 -7
- package/dist/routers/media.mjs.map +1 -1
- package/dist/routers/members.mjs +3 -3
- package/dist/routers/members.mjs.map +1 -1
- package/dist/routers/notification-channels.mjs +18 -25
- package/dist/routers/notification-channels.mjs.map +1 -1
- package/dist/routers/notification-subscriptions.mjs +1 -2
- package/dist/routers/oauth-state.mjs +44 -0
- package/dist/routers/oauth-state.mjs.map +1 -0
- package/dist/routers/preview-base.mjs +37 -0
- package/dist/routers/preview-base.mjs.map +1 -0
- package/dist/routers/preview-token.mjs +50 -0
- package/dist/routers/preview-token.mjs.map +1 -0
- package/dist/routers/profile.mjs +4 -3
- package/dist/routers/profile.mjs.map +1 -1
- package/dist/routers/projects.mjs +18 -3
- package/dist/routers/projects.mjs.map +1 -1
- package/dist/routers/publish-upload.mjs +85 -0
- package/dist/routers/publish-upload.mjs.map +1 -0
- package/dist/routers/published-access.mjs +51 -0
- package/dist/routers/published-access.mjs.map +1 -0
- package/dist/routers/published-compression.mjs +109 -0
- package/dist/routers/published-compression.mjs.map +1 -0
- package/dist/routers/published-files.mjs +69 -0
- package/dist/routers/published-files.mjs.map +1 -0
- package/dist/routers/review-forms.mjs +71 -0
- package/dist/routers/review-forms.mjs.map +1 -0
- package/dist/routers/review-next.mjs +1 -2
- package/dist/routers/review-section.handlers.mjs +84 -0
- package/dist/routers/review-section.handlers.mjs.map +1 -0
- package/dist/routers/review-switch-off.mjs +28 -0
- package/dist/routers/review-switch-off.mjs.map +1 -0
- package/dist/routers/schemas.mjs +141 -24
- package/dist/routers/schemas.mjs.map +1 -1
- package/dist/routers/scoped-models.mjs +21 -0
- package/dist/routers/scoped-models.mjs.map +1 -0
- package/dist/routers/settings-ai.mjs +34 -0
- package/dist/routers/settings-ai.mjs.map +1 -0
- package/dist/routers/settings-capture.mjs +76 -0
- package/dist/routers/settings-capture.mjs.map +1 -0
- package/dist/routers/settings-design.mjs +102 -0
- package/dist/routers/settings-design.mjs.map +1 -0
- package/dist/routers/settings-general.mjs +56 -12
- package/dist/routers/settings-general.mjs.map +1 -1
- package/dist/routers/settings-git-channel.mjs +76 -0
- package/dist/routers/settings-git-channel.mjs.map +1 -0
- package/dist/routers/settings-labels.mjs +28 -3
- package/dist/routers/settings-labels.mjs.map +1 -1
- package/dist/routers/settings-links.mjs +62 -0
- package/dist/routers/settings-links.mjs.map +1 -0
- package/dist/routers/settings-members.mjs +5 -9
- package/dist/routers/settings-members.mjs.map +1 -1
- package/dist/routers/settings-notifications.mjs +16 -12
- package/dist/routers/settings-notifications.mjs.map +1 -1
- package/dist/routers/settings-tokens.mjs +3 -7
- package/dist/routers/settings-tokens.mjs.map +1 -1
- package/dist/routers/settings-webhooks.mjs +8 -26
- package/dist/routers/settings-webhooks.mjs.map +1 -1
- package/dist/routers/settings.handlers.mjs +108 -28
- package/dist/routers/settings.handlers.mjs.map +1 -1
- package/dist/routers/settings.mjs +17 -6
- package/dist/routers/settings.mjs.map +1 -1
- package/dist/routers/signed-storybook.mjs +55 -0
- package/dist/routers/signed-storybook.mjs.map +1 -0
- package/dist/routers/snapshot-groups.mjs +62 -0
- package/dist/routers/snapshot-groups.mjs.map +1 -0
- package/dist/routers/snapshots.mjs +104 -7
- package/dist/routers/snapshots.mjs.map +1 -1
- package/dist/routers/storybook.mjs +57 -104
- package/dist/routers/storybook.mjs.map +1 -1
- package/dist/routers/tokens.mjs +3 -7
- package/dist/routers/tokens.mjs.map +1 -1
- package/dist/routers/ui.mjs +38 -7
- package/dist/routers/ui.mjs.map +1 -1
- package/dist/routers/webhooks.mjs +6 -13
- package/dist/routers/webhooks.mjs.map +1 -1
- package/dist/routers/well-known.mjs +12 -6
- package/dist/routers/well-known.mjs.map +1 -1
- package/dist/routes/api-routes.mjs +92 -0
- package/dist/routes/api-routes.mjs.map +1 -0
- package/dist/routes/classification.mjs +37 -0
- package/dist/routes/classification.mjs.map +1 -0
- package/dist/routes/openapi-classes.mjs +21 -0
- package/dist/routes/openapi-classes.mjs.map +1 -0
- package/dist/routes/page-routes.mjs +103 -0
- package/dist/routes/page-routes.mjs.map +1 -0
- package/dist/runtime.mjs +19 -15
- package/dist/runtime.mjs.map +1 -1
- package/dist/serverless.mjs +4 -0
- package/dist/store.mjs +20 -3
- package/dist/store.mjs.map +1 -1
- package/dist/targets.mjs +45 -0
- package/dist/targets.mjs.map +1 -0
- package/dist/ui/avatar.mjs +1 -2
- package/dist/ui/buttons.mjs +17 -5
- package/dist/ui/buttons.mjs.map +1 -1
- package/dist/ui/code-block.mjs +1 -2
- package/dist/ui/compare-stage.mjs +86 -54
- package/dist/ui/compare-stage.mjs.map +1 -1
- package/dist/ui/csrf-field.mjs +1 -2
- package/dist/ui/css.mjs +1 -2
- package/dist/ui/design-reference.mjs +96 -0
- package/dist/ui/design-reference.mjs.map +1 -0
- package/dist/ui/document.mjs +2 -4
- package/dist/ui/document.mjs.map +1 -1
- package/dist/ui/dropdown.mjs +1 -2
- package/dist/ui/feedback.mjs +1 -2
- package/dist/ui/filter-input.mjs +1 -2
- package/dist/ui/forms.mjs +7 -7
- package/dist/ui/forms.mjs.map +1 -1
- package/dist/ui/icons/icon.mjs +1 -2
- package/dist/ui/icons/paths.mjs +2 -2
- package/dist/ui/icons/paths.mjs.map +1 -1
- package/dist/ui/icons/sprite.mjs +1 -2
- package/dist/ui/kbd.mjs +1 -2
- package/dist/ui/layout.mjs +1 -2
- package/dist/ui/preview-embed.mjs +81 -0
- package/dist/ui/preview-embed.mjs.map +1 -0
- package/dist/ui/progress.mjs +1 -2
- package/dist/ui/segmented.mjs +1 -2
- package/dist/ui/shell/account.mjs +1 -2
- package/dist/ui/shell/bare.mjs +1 -2
- package/dist/ui/shell/boot-script.mjs +1 -2
- package/dist/ui/shell/client-script.mjs +86 -2
- package/dist/ui/shell/client-script.mjs.map +1 -1
- package/dist/ui/shell/nav.mjs +13 -10
- package/dist/ui/shell/nav.mjs.map +1 -1
- package/dist/ui/shell/review-script.mjs +25 -3
- package/dist/ui/shell/review-script.mjs.map +1 -1
- package/dist/ui/shell/sidebar.mjs +3 -4
- package/dist/ui/shell/sidebar.mjs.map +1 -1
- package/dist/ui/shell/topbar.mjs +1 -2
- package/dist/ui/stacks.mjs +1 -2
- package/dist/ui/styles/base.mjs +1 -2
- package/dist/ui/styles/behavior.mjs +1 -2
- package/dist/ui/styles/review-layout.mjs +1 -2
- package/dist/ui/styles/review.mjs +1 -2
- package/dist/ui/styles/tokens.mjs +1 -2
- package/dist/ui/styles.mjs +1 -2
- package/dist/ui/subnav.mjs +1 -2
- package/dist/ui/table.mjs +1 -2
- package/dist/ui/theme.mjs +1 -2
- package/dist/ui/thumbnail.mjs +1 -2
- package/dist/ui/time.mjs +1 -2
- package/package.json +14 -12
- package/dist/asset-paths.mjs +0 -23
- package/dist/asset-paths.mjs.map +0 -1
- package/dist/assets/mark.svg +0 -1
- package/dist/pages/settings-status.mjs +0 -137
- package/dist/pages/settings-status.mjs.map +0 -1
- package/dist/routers/settings-status.mjs +0 -77
- package/dist/routers/settings-status.mjs.map +0 -1
- package/dist/routers/status-configs.mjs +0 -108
- package/dist/routers/status-configs.mjs.map +0 -1
package/dist/notify.mjs
CHANGED
|
@@ -3,48 +3,56 @@ import "node:url";
|
|
|
3
3
|
import.meta.url;
|
|
4
4
|
import { credentialKeys } from "./credential-keys.mjs";
|
|
5
5
|
import { getStore } from "./store.mjs";
|
|
6
|
-
import {
|
|
6
|
+
import { clearPaused, notificationOrigin, warnPaused } from "./notifications-pause.mjs";
|
|
7
|
+
import { MemberModel, NotificationChannelModel, NotificationSubscriptionModel, UserModel } from "@storyshelf/core/models";
|
|
7
8
|
import { emitNotifications, emitSystemNotification } from "@storyshelf/core/adapter/notifier";
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
function
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
9
|
+
// src/notify.ts
|
|
10
|
+
/** Absolute review URL for a project page under the resolved public origin. */
|
|
11
|
+
function reviewUrlFor(origin, slug, suffix) {
|
|
12
|
+
return `${origin}/projects/${slug}${suffix}`;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The origin notifications link to; `undefined` (after one log line per pause) while none is
|
|
16
|
+
* resolved, which pauses every notification (ADR 0014, decision 11).
|
|
17
|
+
*/
|
|
18
|
+
async function originOrPause(deps, event) {
|
|
19
|
+
const origin = await notificationOrigin(deps);
|
|
20
|
+
if (origin === void 0) warnPaused(deps.logger, event);
|
|
21
|
+
else clearPaused(deps.logger);
|
|
22
|
+
return origin;
|
|
23
|
+
}
|
|
24
|
+
/** Brand inputs from UI config plus the notification sender settings. */
|
|
25
|
+
function brandFor(ui, settings, reviewUrl) {
|
|
17
26
|
return {
|
|
18
27
|
name: ui.name,
|
|
19
28
|
logo: ui.logo,
|
|
20
29
|
reviewUrl,
|
|
21
|
-
fromEmail:
|
|
22
|
-
fromName:
|
|
23
|
-
footerText:
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
/** Convert a stored row to a send-ready view (null when undecryptable). */
|
|
27
|
-
function toView(row, model) {
|
|
28
|
-
let secret;
|
|
29
|
-
try {
|
|
30
|
-
secret = row.secretEncrypted ? model.decryptSecret(row) : void 0;
|
|
31
|
-
} catch {
|
|
32
|
-
return null;
|
|
33
|
-
}
|
|
34
|
-
return {
|
|
35
|
-
id: row.id,
|
|
36
|
-
projectId: row.projectId,
|
|
37
|
-
provider: row.provider,
|
|
38
|
-
config: NotificationChannelModel.configOf(row),
|
|
39
|
-
secret,
|
|
40
|
-
events: NotificationChannelModel.eventsOf(row),
|
|
41
|
-
enabled: row.enabled
|
|
30
|
+
fromEmail: settings?.fromEmail,
|
|
31
|
+
fromName: settings?.fromName ?? ui.name,
|
|
32
|
+
footerText: settings?.footerText
|
|
42
33
|
};
|
|
43
34
|
}
|
|
44
35
|
/** Load stored channels as send-ready views (skips undecryptable secrets). */
|
|
45
36
|
async function loadChannels(deps, projectId) {
|
|
46
|
-
const model =
|
|
47
|
-
return (projectId === null ? await model.listSystem() : await model.list(projectId)).filter((row) => row.enabled).flatMap((row) => toView(row
|
|
37
|
+
const model = channelModel(deps);
|
|
38
|
+
return (projectId === null ? await model.listSystem() : await model.list(projectId)).filter((row) => row.enabled).flatMap((row) => model.toView(row) ?? []);
|
|
39
|
+
}
|
|
40
|
+
/** The channel model bound to the server secret. */
|
|
41
|
+
function channelModel(deps) {
|
|
42
|
+
return new NotificationChannelModel(deps.db, void 0, credentialKeys(deps.config));
|
|
43
|
+
}
|
|
44
|
+
/** Record each delivery outcome on its channel row (the last error shown in settings). */
|
|
45
|
+
function recordOutcomes(deps) {
|
|
46
|
+
const model = channelModel(deps);
|
|
47
|
+
return async (outcome) => {
|
|
48
|
+
if (outcome.channelId.startsWith("sub:") || outcome.channelId.startsWith("default:")) return;
|
|
49
|
+
await model.recordOutcome(outcome.channelId, outcome.error).catch((error) => {
|
|
50
|
+
deps.logger.warn({
|
|
51
|
+
err: error,
|
|
52
|
+
channelId: outcome.channelId
|
|
53
|
+
}, "failed to record outcome");
|
|
54
|
+
});
|
|
55
|
+
};
|
|
48
56
|
}
|
|
49
57
|
/** Subscriptions matching an event (enabled + allowlist). */
|
|
50
58
|
function matchingSubs(rows, event) {
|
|
@@ -68,10 +76,37 @@ async function targetsForSub(db, sub, wired) {
|
|
|
68
76
|
enabled: true
|
|
69
77
|
}));
|
|
70
78
|
}
|
|
79
|
+
/** Topics that reach a project's approvers and admins unless they opted out (ADR 0013, decision 4). */
|
|
80
|
+
const DEFAULT_ON_EVENTS = /* @__PURE__ */ new Set(["review:requested"]);
|
|
81
|
+
/** Roles that review builds: the default recipients of {@link DEFAULT_ON_EVENTS}. */
|
|
82
|
+
const REVIEWER_ROLES = /* @__PURE__ */ new Set(["approver", "admin"]);
|
|
83
|
+
/**
|
|
84
|
+
* Reviewers with no subscription row for the project: a default-on topic reaches them by email.
|
|
85
|
+
* Anyone with a row decides for themselves (`enabled` off or a topic list without the event opts
|
|
86
|
+
* out), so a row never adds a second message here.
|
|
87
|
+
*/
|
|
88
|
+
async function defaultTargets(db, projectId, rows, wired) {
|
|
89
|
+
if (!wired.has("email")) return [];
|
|
90
|
+
const subscribed = new Set(rows.map((row) => row.userId));
|
|
91
|
+
const members = (await new MemberModel(db).list(projectId)).filter((member) => REVIEWER_ROLES.has(member.role) && !subscribed.has(member.userId));
|
|
92
|
+
return (await Promise.all(members.map(async (member) => {
|
|
93
|
+
const user = await new UserModel(db).get(member.userId);
|
|
94
|
+
return !user || user.disabled ? [] : [{
|
|
95
|
+
id: `default:${projectId}:${member.userId}`,
|
|
96
|
+
projectId,
|
|
97
|
+
provider: "email",
|
|
98
|
+
config: { to: user.email },
|
|
99
|
+
events: [],
|
|
100
|
+
enabled: true
|
|
101
|
+
}];
|
|
102
|
+
}))).flat();
|
|
103
|
+
}
|
|
71
104
|
/** Per-subscriber targets for an event (only kinds that are wired). */
|
|
72
105
|
async function subscriberTargets(db, projectId, event, wired) {
|
|
73
106
|
const rows = await new NotificationSubscriptionModel(db).list(projectId);
|
|
74
|
-
|
|
107
|
+
const nested = await Promise.all(matchingSubs(rows, event).map(async (sub) => await targetsForSub(db, sub, wired)));
|
|
108
|
+
const defaults = DEFAULT_ON_EVENTS.has(event) ? await defaultTargets(db, projectId, rows, wired) : [];
|
|
109
|
+
return [...nested.flat(), ...defaults];
|
|
75
110
|
}
|
|
76
111
|
/**
|
|
77
112
|
* Fan out a project event to stored channels plus opted-in subscribers.
|
|
@@ -79,7 +114,9 @@ async function subscriberTargets(db, projectId, event, wired) {
|
|
|
79
114
|
*/
|
|
80
115
|
async function notifyProjectWith(deps, project, event, data, reviewSuffix) {
|
|
81
116
|
if (deps.notifiers.length === 0) return;
|
|
82
|
-
const
|
|
117
|
+
const origin = await originOrPause(deps, event);
|
|
118
|
+
if (origin === void 0) return;
|
|
119
|
+
const reviewUrl = reviewSuffix ? reviewUrlFor(origin, project.slug, reviewSuffix) : void 0;
|
|
83
120
|
const wired = new Set(deps.notifiers.map((candidate) => candidate.metadata.kind));
|
|
84
121
|
const channels = await loadChannels(deps, project.id);
|
|
85
122
|
const targets = await subscriberTargets(deps.db, project.id, event, wired);
|
|
@@ -92,8 +129,9 @@ async function notifyProjectWith(deps, project, event, data, reviewSuffix) {
|
|
|
92
129
|
}, {
|
|
93
130
|
notifiers: deps.notifiers,
|
|
94
131
|
channels: [...channels, ...targets],
|
|
95
|
-
brand: brandFor(deps.ui, deps.
|
|
96
|
-
logger: deps.logger
|
|
132
|
+
brand: brandFor(deps.ui, deps.notificationsConfig, reviewUrl),
|
|
133
|
+
logger: deps.logger,
|
|
134
|
+
onOutcome: recordOutcomes(deps)
|
|
97
135
|
});
|
|
98
136
|
}
|
|
99
137
|
/**
|
|
@@ -105,7 +143,7 @@ async function notifyProject(project, event, data, reviewSuffix) {
|
|
|
105
143
|
}
|
|
106
144
|
/** Fan out a site-wide admin alert to project-less channels. */
|
|
107
145
|
async function notifySystemWith(deps, event, data) {
|
|
108
|
-
if (deps.notifiers.length === 0) return;
|
|
146
|
+
if (deps.notifiers.length === 0 || await originOrPause(deps, event) === void 0) return;
|
|
109
147
|
await emitSystemNotification({
|
|
110
148
|
event,
|
|
111
149
|
data,
|
|
@@ -113,8 +151,9 @@ async function notifySystemWith(deps, event, data) {
|
|
|
113
151
|
}, {
|
|
114
152
|
notifiers: deps.notifiers,
|
|
115
153
|
channels: await loadChannels(deps, null),
|
|
116
|
-
brand: brandFor(deps.ui, deps.
|
|
117
|
-
logger: deps.logger
|
|
154
|
+
brand: brandFor(deps.ui, deps.notificationsConfig),
|
|
155
|
+
logger: deps.logger,
|
|
156
|
+
onOutcome: recordOutcomes(deps)
|
|
118
157
|
});
|
|
119
158
|
}
|
|
120
159
|
/** Fan out a site-wide admin alert to project-less channels. */
|
|
@@ -135,7 +174,6 @@ function createAuthSystemHook() {
|
|
|
135
174
|
function now() {
|
|
136
175
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
137
176
|
}
|
|
138
|
-
//#endregion
|
|
139
177
|
export { createAuthSystemHook, notifyProject, notifyProjectWith, notifySystem, notifySystemWith };
|
|
140
178
|
|
|
141
179
|
//# sourceMappingURL=notify.mjs.map
|
package/dist/notify.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"notify.mjs","names":[],"sources":["../src/notify.ts"],"sourcesContent":["/**\n * Notification fan-out for routers: loads stored channels and subscriber\n * emails, then delegates to the wired notifier providers. Best-effort and\n * non-fatal — failures are logged inside the emitters, never thrown here.\n */\nimport type { DatabaseAdapter } from \"@storyshelf/core/adapter/database\";\nimport type { NotifierProvider } from \"@storyshelf/core/adapter/notifier\";\nimport {\n emitNotifications,\n emitSystemNotification,\n type NotificationBrand,\n type NotificationChannel,\n type SystemEventName,\n} from \"@storyshelf/core/adapter/notifier\";\nimport type { ShelfConfig, UIConfig } from \"@storyshelf/core/config\";\nimport type { Logger } from \"@storyshelf/core/logger\";\nimport {\n NotificationChannelModel,\n NotificationSubscriptionModel,\n UserModel,\n} from \"@storyshelf/core/models\";\nimport type { NotificationSubscriptionRow } from \"@storyshelf/core/schema\";\nimport type { NotificationChannelRow } from \"@storyshelf/core/schema\";\nimport type { Project } from \"@storyshelf/core/schema\";\nimport { credentialKeys } from \"./credential-keys.ts\";\nimport { getStore } from \"./store.ts\";\n\n/** Explicit dependencies for background fan-out (no request scope). */\nexport interface NotifyDeps {\n db: DatabaseAdapter;\n config: ShelfConfig;\n ui: UIConfig;\n logger: Logger;\n notifiers: NotifierProvider[];\n}\n\n/** Review URL for a project page (absolute when `publicBaseUrl` is set). */\nexport function reviewUrlFor(slug: string, suffix: string): string | undefined {\n return reviewUrlForWith(getStore().config.publicBaseUrl, slug, suffix);\n}\n\n/** Review URL from an explicit base URL (background use). */\nfunction reviewUrlForWith(\n publicBaseUrl: string | undefined,\n slug: string,\n suffix: string,\n): string | undefined {\n const base = publicBaseUrl?.replace(/\\/+$/u, \"\");\n if (!base) {\n return undefined;\n }\n return `${base}/projects/${slug}${suffix}`;\n}\n\n/** Brand inputs from UI config plus server notification defaults. */\nfunction brandFor(ui: UIConfig, config: ShelfConfig, reviewUrl?: string): NotificationBrand {\n return {\n name: ui.name,\n logo: ui.logo,\n reviewUrl,\n fromEmail: config.notifications?.fromEmail,\n fromName: config.notifications?.fromName ?? ui.name,\n footerText: config.notifications?.footerText,\n };\n}\n\n/** Convert a stored row to a send-ready view (null when undecryptable). */\nfunction toView(\n row: NotificationChannelRow,\n model: NotificationChannelModel,\n): NotificationChannel | null {\n let secret: string | undefined;\n try {\n secret = row.secretEncrypted ? model.decryptSecret(row) : undefined;\n } catch {\n return null;\n }\n return {\n id: row.id,\n projectId: row.projectId,\n provider: row.provider,\n config: NotificationChannelModel.configOf(row),\n secret,\n events: NotificationChannelModel.eventsOf(row),\n enabled: row.enabled,\n };\n}\n\n/** Load stored channels as send-ready views (skips undecryptable secrets). */\nasync function loadChannels(\n deps: Pick<NotifyDeps, \"db\" | \"config\">,\n projectId: string | null,\n): Promise<NotificationChannel[]> {\n const model = new NotificationChannelModel(deps.db, undefined, credentialKeys(deps.config));\n const rows = projectId === null ? await model.listSystem() : await model.list(projectId);\n return rows.filter((row) => row.enabled).flatMap((row) => toView(row, model) ?? []);\n}\n\n/** Subscriptions matching an event (enabled + allowlist). */\nfunction matchingSubs(\n rows: NotificationSubscriptionRow[],\n event: string,\n): NotificationSubscriptionRow[] {\n return rows.filter((row) => {\n if (!row.enabled) {\n return false;\n }\n const events = NotificationSubscriptionModel.eventsOf(row);\n return events.length === 0 || events.includes(event);\n });\n}\n\n/** Per-subscriber targets for one subscription (wired kinds only). */\nasync function targetsForSub(\n db: DatabaseAdapter,\n sub: NotificationSubscriptionRow,\n wired: ReadonlySet<string>,\n): Promise<NotificationChannel[]> {\n const user = await new UserModel(db).get(sub.userId);\n if (!user || user.disabled) {\n return [];\n }\n const kinds = NotificationSubscriptionModel.viaOf(sub);\n const wants = kinds.length > 0 ? kinds : [\"email\"];\n return wants\n .filter((kind) => wired.has(kind))\n .map((kind) => ({\n id: `sub:${sub.id}:${kind}`,\n projectId: sub.projectId,\n provider: kind,\n config: { to: user.email },\n events: [],\n enabled: true,\n }));\n}\n\n/** Per-subscriber targets for an event (only kinds that are wired). */\nasync function subscriberTargets(\n db: DatabaseAdapter,\n projectId: string,\n event: string,\n wired: ReadonlySet<string>,\n): Promise<NotificationChannel[]> {\n const rows = await new NotificationSubscriptionModel(db).list(projectId);\n const nested = await Promise.all(\n matchingSubs(rows, event).map(async (sub) => await targetsForSub(db, sub, wired)),\n );\n return nested.flat();\n}\n\n/**\n * Fan out a project event to stored channels plus opted-in subscribers.\n * Runs alongside (never instead of) webhook delivery.\n */\nexport async function notifyProjectWith(\n deps: NotifyDeps,\n project: Pick<Project, \"id\" | \"slug\">,\n event: string,\n data: Record<string, unknown>,\n reviewSuffix?: string,\n): Promise<void> {\n if (deps.notifiers.length === 0) {\n return;\n }\n const reviewUrl = reviewSuffix\n ? reviewUrlForWith(deps.config.publicBaseUrl, project.slug, reviewSuffix)\n : undefined;\n const wired = new Set(deps.notifiers.map((candidate) => candidate.metadata.kind));\n const channels = await loadChannels(deps, project.id);\n const targets = await subscriberTargets(deps.db, project.id, event, wired);\n await emitNotifications(\n { event, projectId: project.id, projectSlug: project.slug, data, timestamp: now() },\n {\n notifiers: deps.notifiers,\n channels: [...channels, ...targets],\n brand: brandFor(deps.ui, deps.config, reviewUrl),\n logger: deps.logger,\n },\n );\n}\n\n/**\n * Fan out a project event to stored channels plus opted-in subscribers.\n * Runs alongside (never instead of) webhook delivery.\n */\nexport async function notifyProject(\n project: Pick<Project, \"id\" | \"slug\">,\n event: string,\n data: Record<string, unknown>,\n reviewSuffix?: string,\n): Promise<void> {\n await notifyProjectWith(getStore(), project, event, data, reviewSuffix);\n}\n\n/** Fan out a site-wide admin alert to project-less channels. */\nexport async function notifySystemWith(\n deps: NotifyDeps,\n event: SystemEventName,\n data: Record<string, unknown>,\n): Promise<void> {\n if (deps.notifiers.length === 0) {\n return;\n }\n await emitSystemNotification(\n { event, data, timestamp: now() },\n {\n notifiers: deps.notifiers,\n channels: await loadChannels(deps, null),\n brand: brandFor(deps.ui, deps.config),\n logger: deps.logger,\n },\n );\n}\n\n/** Fan out a site-wide admin alert to project-less channels. */\nexport async function notifySystem(\n event: SystemEventName,\n data: Record<string, unknown>,\n): Promise<void> {\n await notifySystemWith(getStore(), event, data);\n}\n\n/**\n * Host hook for the auth engine (`onAuthSystemEvent`): fans `sys:*` auth\n * events out to admin channels. Safe to call anywhere — outside a request\n * scope (e.g. boot) it resolves to a no-op instead of throwing.\n */\nexport function createAuthSystemHook(): (\n event: \"sys:user-created\" | \"sys:invite-issued\",\n data: Record<string, unknown>,\n) => Promise<void> {\n return async (event, data): Promise<void> => {\n await notifySystem(event, data).catch(() => {\n // Intentionally empty — best-effort fan-out must never break auth\n });\n };\n}\n\n/** Current timestamp for notification envelopes. */\nfunction now(): string {\n return new Date().toISOString();\n}\n"],"mappings":";;;;;;;;;AA0CA,SAAS,iBACP,eACA,MACA,QACoB;CACpB,MAAM,OAAO,eAAe,QAAQ,SAAS,EAAE;CAC/C,IAAI,CAAC,MACH;CAEF,OAAO,GAAG,KAAK,YAAY,OAAO;AACpC;;AAGA,SAAS,SAAS,IAAc,QAAqB,WAAuC;CAC1F,OAAO;EACL,MAAM,GAAG;EACT,MAAM,GAAG;EACT;EACA,WAAW,OAAO,eAAe;EACjC,UAAU,OAAO,eAAe,YAAY,GAAG;EAC/C,YAAY,OAAO,eAAe;CACpC;AACF;;AAGA,SAAS,OACP,KACA,OAC4B;CAC5B,IAAI;CACJ,IAAI;EACF,SAAS,IAAI,kBAAkB,MAAM,cAAc,GAAG,IAAI,KAAA;CAC5D,QAAQ;EACN,OAAO;CACT;CACA,OAAO;EACL,IAAI,IAAI;EACR,WAAW,IAAI;EACf,UAAU,IAAI;EACd,QAAQ,yBAAyB,SAAS,GAAG;EAC7C;EACA,QAAQ,yBAAyB,SAAS,GAAG;EAC7C,SAAS,IAAI;CACf;AACF;;AAGA,eAAe,aACb,MACA,WACgC;CAChC,MAAM,QAAQ,IAAI,yBAAyB,KAAK,IAAI,KAAA,GAAW,eAAe,KAAK,MAAM,CAAC;CAE1F,QADa,cAAc,OAAO,MAAM,MAAM,WAAW,IAAI,MAAM,MAAM,KAAK,SAAS,EAAA,CAC3E,QAAQ,QAAQ,IAAI,OAAO,CAAC,CAAC,SAAS,QAAQ,OAAO,KAAK,KAAK,KAAK,CAAC,CAAC;AACpF;;AAGA,SAAS,aACP,MACA,OAC+B;CAC/B,OAAO,KAAK,QAAQ,QAAQ;EAC1B,IAAI,CAAC,IAAI,SACP,OAAO;EAET,MAAM,SAAS,8BAA8B,SAAS,GAAG;EACzD,OAAO,OAAO,WAAW,KAAK,OAAO,SAAS,KAAK;CACrD,CAAC;AACH;;AAGA,eAAe,cACb,IACA,KACA,OACgC;CAChC,MAAM,OAAO,MAAM,IAAI,UAAU,EAAE,CAAC,CAAC,IAAI,IAAI,MAAM;CACnD,IAAI,CAAC,QAAQ,KAAK,UAChB,OAAO,CAAC;CAEV,MAAM,QAAQ,8BAA8B,MAAM,GAAG;CAErD,QADc,MAAM,SAAS,IAAI,QAAQ,CAAC,OAAO,EAAA,CAE9C,QAAQ,SAAS,MAAM,IAAI,IAAI,CAAC,CAAC,CACjC,KAAK,UAAU;EACd,IAAI,OAAO,IAAI,GAAG,GAAG;EACrB,WAAW,IAAI;EACf,UAAU;EACV,QAAQ,EAAE,IAAI,KAAK,MAAM;EACzB,QAAQ,CAAC;EACT,SAAS;CACX,EAAE;AACN;;AAGA,eAAe,kBACb,IACA,WACA,OACA,OACgC;CAChC,MAAM,OAAO,MAAM,IAAI,8BAA8B,EAAE,CAAC,CAAC,KAAK,SAAS;CAIvE,QAAO,MAHc,QAAQ,IAC3B,aAAa,MAAM,KAAK,CAAC,CAAC,IAAI,OAAO,QAAQ,MAAM,cAAc,IAAI,KAAK,KAAK,CAAC,CAClF,EAAA,CACc,KAAK;AACrB;;;;;AAMA,eAAsB,kBACpB,MACA,SACA,OACA,MACA,cACe;CACf,IAAI,KAAK,UAAU,WAAW,GAC5B;CAEF,MAAM,YAAY,eACd,iBAAiB,KAAK,OAAO,eAAe,QAAQ,MAAM,YAAY,IACtE,KAAA;CACJ,MAAM,QAAQ,IAAI,IAAI,KAAK,UAAU,KAAK,cAAc,UAAU,SAAS,IAAI,CAAC;CAChF,MAAM,WAAW,MAAM,aAAa,MAAM,QAAQ,EAAE;CACpD,MAAM,UAAU,MAAM,kBAAkB,KAAK,IAAI,QAAQ,IAAI,OAAO,KAAK;CACzE,MAAM,kBACJ;EAAE;EAAO,WAAW,QAAQ;EAAI,aAAa,QAAQ;EAAM;EAAM,WAAW,IAAI;CAAE,GAClF;EACE,WAAW,KAAK;EAChB,UAAU,CAAC,GAAG,UAAU,GAAG,OAAO;EAClC,OAAO,SAAS,KAAK,IAAI,KAAK,QAAQ,SAAS;EAC/C,QAAQ,KAAK;CACf,CACF;AACF;;;;;AAMA,eAAsB,cACpB,SACA,OACA,MACA,cACe;CACf,MAAM,kBAAkB,SAAS,GAAG,SAAS,OAAO,MAAM,YAAY;AACxE;;AAGA,eAAsB,iBACpB,MACA,OACA,MACe;CACf,IAAI,KAAK,UAAU,WAAW,GAC5B;CAEF,MAAM,uBACJ;EAAE;EAAO;EAAM,WAAW,IAAI;CAAE,GAChC;EACE,WAAW,KAAK;EAChB,UAAU,MAAM,aAAa,MAAM,IAAI;EACvC,OAAO,SAAS,KAAK,IAAI,KAAK,MAAM;EACpC,QAAQ,KAAK;CACf,CACF;AACF;;AAGA,eAAsB,aACpB,OACA,MACe;CACf,MAAM,iBAAiB,SAAS,GAAG,OAAO,IAAI;AAChD;;;;;;AAOA,SAAgB,uBAGG;CACjB,OAAO,OAAO,OAAO,SAAwB;EAC3C,MAAM,aAAa,OAAO,IAAI,CAAC,CAAC,YAAY,CAE5C,CAAC;CACH;AACF;;AAGA,SAAS,MAAc;CACrB,wBAAO,IAAI,KAAK,EAAA,CAAE,YAAY;AAChC"}
|
|
1
|
+
{"version":3,"file":"notify.mjs","names":[],"sources":["../src/notify.ts"],"sourcesContent":["/**\n * Notification fan-out for routers: loads stored channels and subscriber\n * emails, then delegates to the wired notifier providers. Best-effort and\n * non-fatal — failures are logged inside the emitters, never thrown here.\n */\nimport type { DatabaseAdapter } from \"@storyshelf/core/adapter/database\";\nimport type { NotifierProvider } from \"@storyshelf/core/adapter/notifier\";\nimport {\n emitNotifications,\n emitSystemNotification,\n type ChannelOutcome,\n type NotificationBrand,\n type NotificationChannel,\n type SystemEventName,\n} from \"@storyshelf/core/adapter/notifier\";\nimport type { NotificationsConfig, ShelfConfig, UIConfig } from \"@storyshelf/core/config\";\nimport type { Logger } from \"@storyshelf/core/logger\";\nimport {\n MemberModel,\n NotificationChannelModel,\n NotificationSubscriptionModel,\n UserModel,\n type PublicOriginResolver,\n} from \"@storyshelf/core/models\";\nimport type { NotificationSubscriptionRow } from \"@storyshelf/core/schema\";\nimport type { Project } from \"@storyshelf/core/schema\";\nimport { credentialKeys } from \"./credential-keys.ts\";\nimport { clearPaused, notificationOrigin, warnPaused } from \"./notifications-pause.ts\";\nimport { getStore } from \"./store.ts\";\n\n/** Explicit dependencies for background fan-out (no request scope). */\nexport interface NotifyDeps {\n db: DatabaseAdapter;\n config: ShelfConfig;\n ui: UIConfig;\n logger: Logger;\n notifiers: NotifierProvider[];\n /** Brand/sender settings (`notifications.config`). */\n notificationsConfig?: NotificationsConfig;\n /** The public origin resolver; absent ⇒ `config.publicBaseUrl` only. */\n publicOrigin?: Pick<PublicOriginResolver, \"url\"> | undefined;\n}\n\n/** Absolute review URL for a project page under the resolved public origin. */\nfunction reviewUrlFor(origin: string, slug: string, suffix: string): string {\n return `${origin}/projects/${slug}${suffix}`;\n}\n\n/**\n * The origin notifications link to; `undefined` (after one log line per pause) while none is\n * resolved, which pauses every notification (ADR 0014, decision 11).\n */\nasync function originOrPause(\n deps: Pick<NotifyDeps, \"config\" | \"publicOrigin\" | \"logger\">,\n event: string,\n): Promise<string | undefined> {\n const origin = await notificationOrigin(deps);\n if (origin === undefined) {\n warnPaused(deps.logger, event);\n } else {\n clearPaused(deps.logger);\n }\n return origin;\n}\n\n/** Brand inputs from UI config plus the notification sender settings. */\nfunction brandFor(\n ui: UIConfig,\n settings: NotificationsConfig | undefined,\n reviewUrl?: string,\n): NotificationBrand {\n return {\n name: ui.name,\n logo: ui.logo,\n reviewUrl,\n fromEmail: settings?.fromEmail,\n fromName: settings?.fromName ?? ui.name,\n footerText: settings?.footerText,\n };\n}\n\n/** Load stored channels as send-ready views (skips undecryptable secrets). */\nasync function loadChannels(\n deps: Pick<NotifyDeps, \"db\" | \"config\">,\n projectId: string | null,\n): Promise<NotificationChannel[]> {\n const model = channelModel(deps);\n const rows = projectId === null ? await model.listSystem() : await model.list(projectId);\n return rows.filter((row) => row.enabled).flatMap((row) => model.toView(row) ?? []);\n}\n\n/** The channel model bound to the server secret. */\nfunction channelModel(deps: Pick<NotifyDeps, \"db\" | \"config\">): NotificationChannelModel {\n return new NotificationChannelModel(deps.db, undefined, credentialKeys(deps.config));\n}\n\n/** Record each delivery outcome on its channel row (the last error shown in settings). */\nfunction recordOutcomes(\n deps: Pick<NotifyDeps, \"db\" | \"config\" | \"logger\">,\n): (outcome: ChannelOutcome) => Promise<void> {\n const model = channelModel(deps);\n return async (outcome) => {\n if (outcome.channelId.startsWith(\"sub:\") || outcome.channelId.startsWith(\"default:\")) {\n return;\n }\n await model.recordOutcome(outcome.channelId, outcome.error).catch((error: unknown) => {\n deps.logger.warn({ err: error, channelId: outcome.channelId }, \"failed to record outcome\");\n });\n };\n}\n\n/** Subscriptions matching an event (enabled + allowlist). */\nfunction matchingSubs(\n rows: NotificationSubscriptionRow[],\n event: string,\n): NotificationSubscriptionRow[] {\n return rows.filter((row) => {\n if (!row.enabled) {\n return false;\n }\n const events = NotificationSubscriptionModel.eventsOf(row);\n return events.length === 0 || events.includes(event);\n });\n}\n\n/** Per-subscriber targets for one subscription (wired kinds only). */\nasync function targetsForSub(\n db: DatabaseAdapter,\n sub: NotificationSubscriptionRow,\n wired: ReadonlySet<string>,\n): Promise<NotificationChannel[]> {\n const user = await new UserModel(db).get(sub.userId);\n if (!user || user.disabled) {\n return [];\n }\n const kinds = NotificationSubscriptionModel.viaOf(sub);\n const wants = kinds.length > 0 ? kinds : [\"email\"];\n return wants\n .filter((kind) => wired.has(kind))\n .map((kind) => ({\n id: `sub:${sub.id}:${kind}`,\n projectId: sub.projectId,\n provider: kind,\n config: { to: user.email },\n events: [],\n enabled: true,\n }));\n}\n\n/** Topics that reach a project's approvers and admins unless they opted out (ADR 0013, decision 4). */\nconst DEFAULT_ON_EVENTS: ReadonlySet<string> = new Set([\"review:requested\"]);\n\n/** Roles that review builds: the default recipients of {@link DEFAULT_ON_EVENTS}. */\nconst REVIEWER_ROLES: ReadonlySet<string> = new Set([\"approver\", \"admin\"]);\n\n/**\n * Reviewers with no subscription row for the project: a default-on topic reaches them by email.\n * Anyone with a row decides for themselves (`enabled` off or a topic list without the event opts\n * out), so a row never adds a second message here.\n */\nasync function defaultTargets(\n db: DatabaseAdapter,\n projectId: string,\n rows: NotificationSubscriptionRow[],\n wired: ReadonlySet<string>,\n): Promise<NotificationChannel[]> {\n if (!wired.has(\"email\")) {\n return [];\n }\n const subscribed = new Set(rows.map((row) => row.userId));\n const members = (await new MemberModel(db).list(projectId)).filter(\n (member) => REVIEWER_ROLES.has(member.role) && !subscribed.has(member.userId),\n );\n const targets = await Promise.all(\n members.map(async (member): Promise<NotificationChannel[]> => {\n const user = await new UserModel(db).get(member.userId);\n return !user || user.disabled\n ? []\n : [\n {\n id: `default:${projectId}:${member.userId}`,\n projectId,\n provider: \"email\",\n config: { to: user.email },\n events: [],\n enabled: true,\n },\n ];\n }),\n );\n return targets.flat();\n}\n\n/** Per-subscriber targets for an event (only kinds that are wired). */\nasync function subscriberTargets(\n db: DatabaseAdapter,\n projectId: string,\n event: string,\n wired: ReadonlySet<string>,\n): Promise<NotificationChannel[]> {\n const rows = await new NotificationSubscriptionModel(db).list(projectId);\n const nested = await Promise.all(\n matchingSubs(rows, event).map(async (sub) => await targetsForSub(db, sub, wired)),\n );\n const defaults = DEFAULT_ON_EVENTS.has(event)\n ? await defaultTargets(db, projectId, rows, wired)\n : [];\n return [...nested.flat(), ...defaults];\n}\n\n/**\n * Fan out a project event to stored channels plus opted-in subscribers.\n * Runs alongside (never instead of) webhook delivery.\n */\nexport async function notifyProjectWith(\n deps: NotifyDeps,\n project: Pick<Project, \"id\" | \"slug\">,\n event: string,\n data: Record<string, unknown>,\n reviewSuffix?: string,\n): Promise<void> {\n if (deps.notifiers.length === 0) {\n return;\n }\n const origin = await originOrPause(deps, event);\n if (origin === undefined) {\n return;\n }\n const reviewUrl = reviewSuffix ? reviewUrlFor(origin, project.slug, reviewSuffix) : undefined;\n const wired = new Set(deps.notifiers.map((candidate) => candidate.metadata.kind));\n const channels = await loadChannels(deps, project.id);\n const targets = await subscriberTargets(deps.db, project.id, event, wired);\n await emitNotifications(\n { event, projectId: project.id, projectSlug: project.slug, data, timestamp: now() },\n {\n notifiers: deps.notifiers,\n channels: [...channels, ...targets],\n brand: brandFor(deps.ui, deps.notificationsConfig, reviewUrl),\n logger: deps.logger,\n onOutcome: recordOutcomes(deps),\n },\n );\n}\n\n/**\n * Fan out a project event to stored channels plus opted-in subscribers.\n * Runs alongside (never instead of) webhook delivery.\n */\nexport async function notifyProject(\n project: Pick<Project, \"id\" | \"slug\">,\n event: string,\n data: Record<string, unknown>,\n reviewSuffix?: string,\n): Promise<void> {\n await notifyProjectWith(getStore(), project, event, data, reviewSuffix);\n}\n\n/** Fan out a site-wide admin alert to project-less channels. */\nexport async function notifySystemWith(\n deps: NotifyDeps,\n event: SystemEventName,\n data: Record<string, unknown>,\n): Promise<void> {\n if (deps.notifiers.length === 0 || (await originOrPause(deps, event)) === undefined) {\n return;\n }\n await emitSystemNotification(\n { event, data, timestamp: now() },\n {\n notifiers: deps.notifiers,\n channels: await loadChannels(deps, null),\n brand: brandFor(deps.ui, deps.notificationsConfig),\n logger: deps.logger,\n onOutcome: recordOutcomes(deps),\n },\n );\n}\n\n/** Fan out a site-wide admin alert to project-less channels. */\nexport async function notifySystem(\n event: SystemEventName,\n data: Record<string, unknown>,\n): Promise<void> {\n await notifySystemWith(getStore(), event, data);\n}\n\n/**\n * Host hook for the auth engine (`onAuthSystemEvent`): fans `sys:*` auth\n * events out to admin channels. Safe to call anywhere — outside a request\n * scope (e.g. boot) it resolves to a no-op instead of throwing.\n */\nexport function createAuthSystemHook(): (\n event: \"sys:user-created\" | \"sys:invite-issued\",\n data: Record<string, unknown>,\n) => Promise<void> {\n return async (event, data): Promise<void> => {\n await notifySystem(event, data).catch(() => {\n // Intentionally empty — best-effort fan-out must never break auth\n });\n };\n}\n\n/** Current timestamp for notification envelopes. */\nfunction now(): string {\n return new Date().toISOString();\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { Alert, Badge, Meta } from "../ui/feedback.mjs";
|
|
5
|
+
import { jsx, jsxs } from "hono/jsx/jsx-runtime";
|
|
6
|
+
// src/pages/a11y-violations.tsx
|
|
7
|
+
/** The axe violation texts stored on a snapshot (`rule (impact): help [selector] n node(s)`); empty when none. */
|
|
8
|
+
function a11yViolationsOf(snapshot) {
|
|
9
|
+
if (!snapshot.a11yViolations) return [];
|
|
10
|
+
try {
|
|
11
|
+
const parsed = JSON.parse(snapshot.a11yViolations);
|
|
12
|
+
return Array.isArray(parsed) ? parsed.filter((entry) => typeof entry === "string") : [];
|
|
13
|
+
} catch {
|
|
14
|
+
return [];
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
const UNAVAILABLE_PREFIX = "axe unavailable: ";
|
|
18
|
+
/**
|
|
19
|
+
* Whether a stored entry is the marker a runner writes when axe could not run
|
|
20
|
+
* (`axe unavailable: <reason>`). It is a note about the run, never a finding: the one place that
|
|
21
|
+
* knows the marker's shape.
|
|
22
|
+
*/
|
|
23
|
+
function isAxeUnavailable(text) {
|
|
24
|
+
return text.startsWith(UNAVAILABLE_PREFIX);
|
|
25
|
+
}
|
|
26
|
+
/** The entries of a snapshot that are findings (the `axe unavailable` markers left out). */
|
|
27
|
+
function a11yFindingsOf(snapshot) {
|
|
28
|
+
return a11yViolationsOf(snapshot).filter((text) => !isAxeUnavailable(text));
|
|
29
|
+
}
|
|
30
|
+
/** The reason axe did not run on a snapshot, or `undefined` when it ran (or nothing is recorded). */
|
|
31
|
+
function a11yUnavailableReasonOf(snapshot) {
|
|
32
|
+
return a11yViolationsOf(snapshot).find((text) => isAxeUnavailable(text))?.slice(17);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The snapshot that carries the axe findings of a story across its browser targets: axe runs on the
|
|
36
|
+
* primary target only, so the findings sit on one sibling. The given snapshot first, else the first
|
|
37
|
+
* sibling with findings, else whichever carries an `axe unavailable` note, else the given one.
|
|
38
|
+
*/
|
|
39
|
+
function a11yCarrierOf(snapshot, siblings) {
|
|
40
|
+
const found = (candidate) => a11yFindingsOf(candidate).length > 0;
|
|
41
|
+
const unavailable = (candidate) => a11yUnavailableReasonOf(candidate) !== void 0;
|
|
42
|
+
if (found(snapshot)) return snapshot;
|
|
43
|
+
return siblings.find((sibling) => found(sibling)) ?? (unavailable(snapshot) ? snapshot : siblings.find((sibling) => unavailable(sibling))) ?? snapshot;
|
|
44
|
+
}
|
|
45
|
+
const OVERFLOW_LINE = /^…and (\d+) more$/u;
|
|
46
|
+
/** Violations behind a stored list: its overflow line `…and N more` stands for N omitted violations. */
|
|
47
|
+
function a11yCountOf(snapshot) {
|
|
48
|
+
let count = 0;
|
|
49
|
+
for (const text of a11yFindingsOf(snapshot)) {
|
|
50
|
+
const omitted = OVERFLOW_LINE.exec(text)?.[1];
|
|
51
|
+
count += omitted === void 0 ? 1 : Number(omitted);
|
|
52
|
+
}
|
|
53
|
+
return count;
|
|
54
|
+
}
|
|
55
|
+
/** The build-level totals of the findings stored on `snapshots`. */
|
|
56
|
+
function a11ySummaryOf(snapshots) {
|
|
57
|
+
const counts = snapshots.map((snapshot) => a11yCountOf(snapshot));
|
|
58
|
+
return {
|
|
59
|
+
findings: counts.reduce((sum, count) => sum + count, 0),
|
|
60
|
+
snapshots: counts.filter((count) => count > 0).length,
|
|
61
|
+
unavailable: snapshots.filter((snapshot) => a11yUnavailableReasonOf(snapshot) !== void 0).length
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The line under the build-level total. A snapshot stores nothing when axe found nothing and when it
|
|
66
|
+
* did not run (a11y off, capture unfinished), so a zero never claims "none found".
|
|
67
|
+
*/
|
|
68
|
+
function a11ySummaryNote(summary) {
|
|
69
|
+
const base = summary.findings > 0 ? `On ${summary.snapshots} ${summary.snapshots === 1 ? "snapshot" : "snapshots"}. Information only; they never change the status.` : "No findings recorded. Information only.";
|
|
70
|
+
return summary.unavailable > 0 ? `${base} ${a11yUnavailableNote(summary.unavailable)}` : base;
|
|
71
|
+
}
|
|
72
|
+
/** The neutral line for runs where axe did not run; the reason sits on each snapshot. */
|
|
73
|
+
function a11yUnavailableNote(count) {
|
|
74
|
+
return `axe did not run on ${count} ${count === 1 ? "snapshot" : "snapshots"}.`;
|
|
75
|
+
}
|
|
76
|
+
/** " on <target>" for the findings of a snapshot: axe runs on the primary target only, so say which. */
|
|
77
|
+
function onTarget(snapshot) {
|
|
78
|
+
return snapshot.browser ? ` on ${snapshot.browser}` : "";
|
|
79
|
+
}
|
|
80
|
+
/** Review-page note above the viewer: what axe found on this snapshot. It never blocks the build. */
|
|
81
|
+
function A11yViolationsNotice(props) {
|
|
82
|
+
const violations = a11yFindingsOf(props.snapshot);
|
|
83
|
+
const unavailable = a11yUnavailableReasonOf(props.snapshot);
|
|
84
|
+
if (violations.length === 0) return unavailable === void 0 ? null : /* @__PURE__ */ jsx(Alert, {
|
|
85
|
+
tone: "info",
|
|
86
|
+
title: `Accessibility: axe did not run${onTarget(props.snapshot)}`,
|
|
87
|
+
children: /* @__PURE__ */ jsx("code", { children: unavailable })
|
|
88
|
+
});
|
|
89
|
+
return /* @__PURE__ */ jsx(Alert, {
|
|
90
|
+
tone: "warning",
|
|
91
|
+
title: `Accessibility: ${a11yCountOf(props.snapshot)} found${onTarget(props.snapshot)} (does not block)`,
|
|
92
|
+
children: /* @__PURE__ */ jsx("ul", { children: violations.map((text) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx("code", { children: text }) }, text)) })
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
/** Compact card version: a badge with the count and the findings behind a disclosure. */
|
|
96
|
+
function A11yViolationsDetails(props) {
|
|
97
|
+
const violations = a11yFindingsOf(props.snapshot);
|
|
98
|
+
const unavailable = a11yUnavailableReasonOf(props.snapshot);
|
|
99
|
+
if (violations.length === 0) return unavailable === void 0 ? null : /* @__PURE__ */ jsxs("details", { children: [/* @__PURE__ */ jsx("summary", { children: /* @__PURE__ */ jsxs(Badge, {
|
|
100
|
+
tone: "neutral",
|
|
101
|
+
children: ["axe did not run", onTarget(props.snapshot)]
|
|
102
|
+
}) }), /* @__PURE__ */ jsx(Meta, { children: /* @__PURE__ */ jsx("code", { children: unavailable }) })] });
|
|
103
|
+
return /* @__PURE__ */ jsxs("details", { children: [/* @__PURE__ */ jsx("summary", { children: /* @__PURE__ */ jsxs(Badge, {
|
|
104
|
+
tone: "warning",
|
|
105
|
+
icon: true,
|
|
106
|
+
children: [
|
|
107
|
+
"a11y ",
|
|
108
|
+
a11yCountOf(props.snapshot),
|
|
109
|
+
onTarget(props.snapshot)
|
|
110
|
+
]
|
|
111
|
+
}) }), violations.map((text) => /* @__PURE__ */ jsx(Meta, { children: /* @__PURE__ */ jsx("code", { children: text }) }, text))] });
|
|
112
|
+
}
|
|
113
|
+
export { A11yViolationsDetails, A11yViolationsNotice, a11yCarrierOf, a11yCountOf, a11yFindingsOf, a11ySummaryNote, a11ySummaryOf, a11yUnavailableNote, a11yUnavailableReasonOf, a11yViolationsOf, isAxeUnavailable };
|
|
114
|
+
|
|
115
|
+
//# sourceMappingURL=a11y-violations.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"a11y-violations.mjs","names":[],"sources":["../../src/pages/a11y-violations.tsx"],"sourcesContent":["import type { Snapshot } from \"@storyshelf/core/schema\";\nimport type { HtmlEscapedString } from \"hono/utils/html\";\nimport { Alert, Badge, Meta } from \"../ui/components.tsx\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\ntype Rendered = HtmlEscapedString | Promise<HtmlEscapedString>;\n\n/** The axe violation texts stored on a snapshot (`rule (impact): help [selector] n node(s)`); empty when none. */\nexport function a11yViolationsOf(snapshot: Pick<Snapshot, \"a11yViolations\">): string[] {\n if (!snapshot.a11yViolations) {\n return [];\n }\n try {\n const parsed: unknown = JSON.parse(snapshot.a11yViolations);\n return Array.isArray(parsed)\n ? parsed.filter((entry): entry is string => typeof entry === \"string\")\n : [];\n } catch {\n return [];\n }\n}\n\nconst UNAVAILABLE_PREFIX = \"axe unavailable: \";\n\n/**\n * Whether a stored entry is the marker a runner writes when axe could not run\n * (`axe unavailable: <reason>`). It is a note about the run, never a finding: the one place that\n * knows the marker's shape.\n */\nexport function isAxeUnavailable(text: string): boolean {\n return text.startsWith(UNAVAILABLE_PREFIX);\n}\n\n/** The entries of a snapshot that are findings (the `axe unavailable` markers left out). */\nexport function a11yFindingsOf(snapshot: Pick<Snapshot, \"a11yViolations\">): string[] {\n return a11yViolationsOf(snapshot).filter((text) => !isAxeUnavailable(text));\n}\n\n/** The reason axe did not run on a snapshot, or `undefined` when it ran (or nothing is recorded). */\nexport function a11yUnavailableReasonOf(\n snapshot: Pick<Snapshot, \"a11yViolations\">,\n): string | undefined {\n const marker = a11yViolationsOf(snapshot).find((text) => isAxeUnavailable(text));\n return marker?.slice(UNAVAILABLE_PREFIX.length);\n}\n\n/**\n * The snapshot that carries the axe findings of a story across its browser targets: axe runs on the\n * primary target only, so the findings sit on one sibling. The given snapshot first, else the first\n * sibling with findings, else whichever carries an `axe unavailable` note, else the given one.\n */\nexport function a11yCarrierOf<T extends Pick<Snapshot, \"a11yViolations\">>(\n snapshot: T,\n siblings: readonly T[],\n): T {\n const found = (candidate: T): boolean => a11yFindingsOf(candidate).length > 0;\n const unavailable = (candidate: T): boolean => a11yUnavailableReasonOf(candidate) !== undefined;\n if (found(snapshot)) {\n return snapshot;\n }\n // Findings win over an `axe unavailable` note, which only shows when nothing found anything.\n return (\n siblings.find((sibling) => found(sibling)) ??\n (unavailable(snapshot) ? snapshot : siblings.find((sibling) => unavailable(sibling))) ??\n snapshot\n );\n}\n\n/** What axe found across a build, for its summary: information only, never part of a status. */\nexport interface A11ySummary {\n /** Findings summed over every snapshot (axe runs on the primary target only, so none repeat). */\n findings: number;\n /** Snapshots that carry at least one finding. */\n snapshots: number;\n /** Snapshots on which axe did not run (`axe unavailable`); never counted as findings. */\n unavailable: number;\n}\n\nconst OVERFLOW_LINE = /^…and (\\d+) more$/u;\n\n/** Violations behind a stored list: its overflow line `…and N more` stands for N omitted violations. */\nexport function a11yCountOf(snapshot: Pick<Snapshot, \"a11yViolations\">): number {\n let count = 0;\n for (const text of a11yFindingsOf(snapshot)) {\n const omitted = OVERFLOW_LINE.exec(text)?.[1];\n count += omitted === undefined ? 1 : Number(omitted);\n }\n return count;\n}\n\n/** The build-level totals of the findings stored on `snapshots`. */\nexport function a11ySummaryOf(snapshots: readonly Pick<Snapshot, \"a11yViolations\">[]): A11ySummary {\n const counts = snapshots.map((snapshot) => a11yCountOf(snapshot));\n return {\n findings: counts.reduce((sum, count) => sum + count, 0),\n snapshots: counts.filter((count) => count > 0).length,\n unavailable: snapshots.filter((snapshot) => a11yUnavailableReasonOf(snapshot) !== undefined)\n .length,\n };\n}\n\n/**\n * The line under the build-level total. A snapshot stores nothing when axe found nothing and when it\n * did not run (a11y off, capture unfinished), so a zero never claims \"none found\".\n */\nexport function a11ySummaryNote(summary: A11ySummary): string {\n const base =\n summary.findings > 0\n ? `On ${summary.snapshots} ${summary.snapshots === 1 ? \"snapshot\" : \"snapshots\"}. Information only; they never change the status.`\n : \"No findings recorded. Information only.\";\n return summary.unavailable > 0 ? `${base} ${a11yUnavailableNote(summary.unavailable)}` : base;\n}\n\n/** The neutral line for runs where axe did not run; the reason sits on each snapshot. */\nexport function a11yUnavailableNote(count: number): string {\n return `axe did not run on ${count} ${count === 1 ? \"snapshot\" : \"snapshots\"}.`;\n}\n\n/** \" on <target>\" for the findings of a snapshot: axe runs on the primary target only, so say which. */\nfunction onTarget(snapshot: Pick<Snapshot, \"browser\">): string {\n return snapshot.browser ? ` on ${snapshot.browser}` : \"\";\n}\n\n/** Review-page note above the viewer: what axe found on this snapshot. It never blocks the build. */\nexport function A11yViolationsNotice(props: {\n snapshot: Pick<Snapshot, \"a11yViolations\" | \"browser\">;\n}): Rendered | null {\n const violations = a11yFindingsOf(props.snapshot);\n const unavailable = a11yUnavailableReasonOf(props.snapshot);\n if (violations.length === 0) {\n return unavailable === undefined ? null : (\n <Alert tone=\"info\" title={`Accessibility: axe did not run${onTarget(props.snapshot)}`}>\n <code>{unavailable}</code>\n </Alert>\n );\n }\n return (\n <Alert\n tone=\"warning\"\n title={`Accessibility: ${a11yCountOf(props.snapshot)} found${onTarget(props.snapshot)} (does not block)`}\n >\n <ul>\n {violations.map((text) => (\n <li key={text}>\n <code>{text}</code>\n </li>\n ))}\n </ul>\n </Alert>\n );\n}\n\n/** Compact card version: a badge with the count and the findings behind a disclosure. */\nexport function A11yViolationsDetails(props: {\n snapshot: Pick<Snapshot, \"a11yViolations\" | \"browser\">;\n}): Rendered | null {\n const violations = a11yFindingsOf(props.snapshot);\n const unavailable = a11yUnavailableReasonOf(props.snapshot);\n if (violations.length === 0) {\n return unavailable === undefined ? null : (\n <details>\n <summary>\n <Badge tone=\"neutral\">axe did not run{onTarget(props.snapshot)}</Badge>\n </summary>\n <Meta>\n <code>{unavailable}</code>\n </Meta>\n </details>\n );\n }\n return (\n <details>\n <summary>\n <Badge tone=\"warning\" icon>\n a11y {a11yCountOf(props.snapshot)}\n {onTarget(props.snapshot)}\n </Badge>\n </summary>\n {violations.map((text) => (\n <Meta key={text}>\n <code>{text}</code>\n </Meta>\n ))}\n </details>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { EmptyState, Meta, Stat } from "../ui/feedback.mjs";
|
|
5
|
+
import { Card, PageHeader, SectionTitle } from "../ui/layout.mjs";
|
|
6
|
+
import { Table } from "../ui/table.mjs";
|
|
7
|
+
import { DocumentLayout } from "../ui/document.mjs";
|
|
8
|
+
import { Fragment, jsx, jsxs } from "hono/jsx/jsx-runtime";
|
|
9
|
+
// src/pages/admin-ai.tsx
|
|
10
|
+
function DaysTable(props) {
|
|
11
|
+
return /* @__PURE__ */ jsx(Table, {
|
|
12
|
+
dense: true,
|
|
13
|
+
children: /* @__PURE__ */ jsxs("table", { children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
|
|
14
|
+
/* @__PURE__ */ jsx("th", { children: "Day" }),
|
|
15
|
+
/* @__PURE__ */ jsx("th", { children: "Tokens" }),
|
|
16
|
+
/* @__PURE__ */ jsx("th", { children: "Calls" }),
|
|
17
|
+
/* @__PURE__ */ jsx("th", { children: "Failed" })
|
|
18
|
+
] }) }), /* @__PURE__ */ jsx("tbody", { children: props.days.map((day) => /* @__PURE__ */ jsxs("tr", { children: [
|
|
19
|
+
/* @__PURE__ */ jsx("td", { children: day.day }),
|
|
20
|
+
/* @__PURE__ */ jsx("td", { children: day.tokens }),
|
|
21
|
+
/* @__PURE__ */ jsx("td", { children: day.calls }),
|
|
22
|
+
/* @__PURE__ */ jsx("td", { children: day.failed })
|
|
23
|
+
] })) })] })
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
function ProfilesTable(props) {
|
|
27
|
+
return /* @__PURE__ */ jsx(Table, {
|
|
28
|
+
dense: true,
|
|
29
|
+
children: /* @__PURE__ */ jsxs("table", { children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
|
|
30
|
+
/* @__PURE__ */ jsx("th", { children: "Profile" }),
|
|
31
|
+
/* @__PURE__ */ jsx("th", { children: "Tokens" }),
|
|
32
|
+
/* @__PURE__ */ jsx("th", { children: "Calls" })
|
|
33
|
+
] }) }), /* @__PURE__ */ jsx("tbody", { children: props.profiles.map((profile) => /* @__PURE__ */ jsxs("tr", { children: [
|
|
34
|
+
/* @__PURE__ */ jsx("td", { children: profile.profile }),
|
|
35
|
+
/* @__PURE__ */ jsx("td", { children: profile.tokens }),
|
|
36
|
+
/* @__PURE__ */ jsx("td", { children: profile.calls })
|
|
37
|
+
] })) })] })
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
/** Site-admin AI usage page: today's budget position and the last days by profile. */
|
|
41
|
+
function renderAdminAiPage(report) {
|
|
42
|
+
const cap = report.dailyTokens;
|
|
43
|
+
const today = cap === null ? `${report.todayTokens} tokens` : `${report.todayTokens} / ${cap} tokens`;
|
|
44
|
+
return /* @__PURE__ */ jsxs(DocumentLayout, {
|
|
45
|
+
title: "AI usage",
|
|
46
|
+
nav: { active: "admin" },
|
|
47
|
+
children: [
|
|
48
|
+
/* @__PURE__ */ jsx(PageHeader, {
|
|
49
|
+
title: "AI usage",
|
|
50
|
+
description: "Tokens counted toward the daily budget (UTC days). Visible to site admins only."
|
|
51
|
+
}),
|
|
52
|
+
/* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsx(Stat, {
|
|
53
|
+
label: "Today",
|
|
54
|
+
value: today
|
|
55
|
+
}) }),
|
|
56
|
+
report.days.length === 0 ? /* @__PURE__ */ jsx(EmptyState, {
|
|
57
|
+
title: "No usage yet",
|
|
58
|
+
description: "AI calls appear here after the first insight runs."
|
|
59
|
+
}) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
60
|
+
/* @__PURE__ */ jsx(SectionTitle, {
|
|
61
|
+
level: 3,
|
|
62
|
+
children: "By day"
|
|
63
|
+
}),
|
|
64
|
+
/* @__PURE__ */ jsx(DaysTable, { days: report.days }),
|
|
65
|
+
/* @__PURE__ */ jsx(SectionTitle, {
|
|
66
|
+
level: 3,
|
|
67
|
+
children: "By profile"
|
|
68
|
+
}),
|
|
69
|
+
/* @__PURE__ */ jsx(ProfilesTable, { profiles: report.profiles }),
|
|
70
|
+
/* @__PURE__ */ jsx(Meta, { children: "Rows older than 90 days are purged. Failed calls the provider billed still count." })
|
|
71
|
+
] })
|
|
72
|
+
]
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
export { renderAdminAiPage };
|
|
76
|
+
|
|
77
|
+
//# sourceMappingURL=admin-ai.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admin-ai.mjs","names":[],"sources":["../../src/pages/admin-ai.tsx"],"sourcesContent":["import type { HtmlEscapedString } from \"hono/utils/html\";\nimport type { UsageReport } from \"../insights/usage-report.ts\";\nimport {\n Card,\n EmptyState,\n Meta,\n PageHeader,\n SectionTitle,\n Stat,\n Table,\n} from \"../ui/components.tsx\";\nimport { DocumentLayout, type RenderedContent } from \"../ui/document.tsx\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\nfunction DaysTable(props: {\n days: UsageReport[\"days\"];\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n return (\n <Table dense>\n <table>\n <thead>\n <tr>\n <th>Day</th>\n <th>Tokens</th>\n <th>Calls</th>\n <th>Failed</th>\n </tr>\n </thead>\n <tbody>\n {props.days.map((day) => (\n <tr>\n <td>{day.day}</td>\n <td>{day.tokens}</td>\n <td>{day.calls}</td>\n <td>{day.failed}</td>\n </tr>\n ))}\n </tbody>\n </table>\n </Table>\n );\n}\n\nfunction ProfilesTable(props: {\n profiles: UsageReport[\"profiles\"];\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n return (\n <Table dense>\n <table>\n <thead>\n <tr>\n <th>Profile</th>\n <th>Tokens</th>\n <th>Calls</th>\n </tr>\n </thead>\n <tbody>\n {props.profiles.map((profile) => (\n <tr>\n <td>{profile.profile}</td>\n <td>{profile.tokens}</td>\n <td>{profile.calls}</td>\n </tr>\n ))}\n </tbody>\n </table>\n </Table>\n );\n}\n\n/** Site-admin AI usage page: today's budget position and the last days by profile. */\nexport function renderAdminAiPage(report: UsageReport): RenderedContent {\n const cap = report.dailyTokens;\n const today =\n cap === null ? `${report.todayTokens} tokens` : `${report.todayTokens} / ${cap} tokens`;\n return (\n <DocumentLayout title=\"AI usage\" nav={{ active: \"admin\" }}>\n <PageHeader\n title=\"AI usage\"\n description=\"Tokens counted toward the daily budget (UTC days). Visible to site admins only.\"\n />\n <Card>\n <Stat label=\"Today\" value={today} />\n </Card>\n {report.days.length === 0 ? (\n <EmptyState\n title=\"No usage yet\"\n description=\"AI calls appear here after the first insight runs.\"\n />\n ) : (\n <>\n <SectionTitle level={3}>By day</SectionTitle>\n <DaysTable days={report.days} />\n <SectionTitle level={3}>By profile</SectionTitle>\n <ProfilesTable profiles={report.profiles} />\n <Meta>\n Rows older than 90 days are purged. Failed calls the provider billed still count.\n </Meta>\n </>\n )}\n </DocumentLayout>\n );\n}\n"],"mappings":";;;;;;;;;AAeA,SAAS,UAAU,OAEgC;CACjD,OACE,oBAAC,OAAD;EAAO,OAAA;EACL,UAAA,qBAAC,SAAD,EAAA,UAAA,CACE,oBAAC,SAAD,EAAA,UACE,qBAAC,MAAD,EAAA,UAAA;GACE,oBAAC,MAAD,EAAA,UAAI,MAAO,CAAA;GACX,oBAAC,MAAD,EAAA,UAAI,SAAU,CAAA;GACd,oBAAC,MAAD,EAAA,UAAI,QAAS,CAAA;GACb,oBAAC,MAAD,EAAA,UAAI,SAAU,CAAA;EACZ,EAAA,CAAA,EACC,CAAA,GACP,oBAAC,SAAD,EAAA,UACG,MAAM,KAAK,KAAK,QACf,qBAAC,MAAD,EAAA,UAAA;GACE,oBAAC,MAAD,EAAA,UAAK,IAAI,IAAQ,CAAA;GACjB,oBAAC,MAAD,EAAA,UAAK,IAAI,OAAW,CAAA;GACpB,oBAAC,MAAD,EAAA,UAAK,IAAI,MAAU,CAAA;GACnB,oBAAC,MAAD,EAAA,UAAK,IAAI,OAAW,CAAA;EAClB,EAAA,CAAA,CACL,EACI,CAAA,CACF,EAAA,CAAA;CACF,CAAA;AAEX;AAEA,SAAS,cAAc,OAE4B;CACjD,OACE,oBAAC,OAAD;EAAO,OAAA;EACL,UAAA,qBAAC,SAAD,EAAA,UAAA,CACE,oBAAC,SAAD,EAAA,UACE,qBAAC,MAAD,EAAA,UAAA;GACE,oBAAC,MAAD,EAAA,UAAI,UAAW,CAAA;GACf,oBAAC,MAAD,EAAA,UAAI,SAAU,CAAA;GACd,oBAAC,MAAD,EAAA,UAAI,QAAS,CAAA;EACX,EAAA,CAAA,EACC,CAAA,GACP,oBAAC,SAAD,EAAA,UACG,MAAM,SAAS,KAAK,YACnB,qBAAC,MAAD,EAAA,UAAA;GACE,oBAAC,MAAD,EAAA,UAAK,QAAQ,QAAY,CAAA;GACzB,oBAAC,MAAD,EAAA,UAAK,QAAQ,OAAW,CAAA;GACxB,oBAAC,MAAD,EAAA,UAAK,QAAQ,MAAU,CAAA;EACrB,EAAA,CAAA,CACL,EACI,CAAA,CACF,EAAA,CAAA;CACF,CAAA;AAEX;;AAGA,SAAgB,kBAAkB,QAAsC;CACtE,MAAM,MAAM,OAAO;CACnB,MAAM,QACJ,QAAQ,OAAO,GAAG,OAAO,YAAY,WAAW,GAAG,OAAO,YAAY,KAAK,IAAI;CACjF,OACE,qBAAC,gBAAD;EAAgB,OAAM;EAAW,KAAK,EAAE,QAAQ,QAAQ;EAAxD,UAAA;GACE,oBAAC,YAAD;IACE,OAAM;IACN,aAAY;GACb,CAAA;GACD,oBAAC,MAAD,EAAA,UACE,oBAAC,MAAD;IAAM,OAAM;IAAQ,OAAO;GAAQ,CAAA,EAC/B,CAAA;GACL,OAAO,KAAK,WAAW,IACtB,oBAAC,YAAD;IACE,OAAM;IACN,aAAY;GACb,CAAA,IAED,qBAAA,UAAA,EAAA,UAAA;IACE,oBAAC,cAAD;KAAc,OAAO;KAAG,UAAA;IAAoB,CAAA;IAC5C,oBAAC,WAAD,EAAW,MAAM,OAAO,KAAO,CAAA;IAC/B,oBAAC,cAAD;KAAc,OAAO;KAAG,UAAA;IAAwB,CAAA;IAChD,oBAAC,eAAD,EAAe,UAAU,OAAO,SAAW,CAAA;IAC3C,oBAAC,MAAD,EAAA,UAAM,oFAEA,CAAA;GACN,EAAA,CAAA;EAEU;;AAEpB"}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { Button } from "../ui/buttons.mjs";
|
|
5
|
+
import { Alert, Badge, Meta } from "../ui/feedback.mjs";
|
|
6
|
+
import { Field, FormActions } from "../ui/forms.mjs";
|
|
7
|
+
import { Card, SectionTitle } from "../ui/layout.mjs";
|
|
8
|
+
import { HStack } from "../ui/stacks.mjs";
|
|
9
|
+
import { csrfField } from "../ui/csrf-field.mjs";
|
|
10
|
+
import { Fragment, jsx, jsxs } from "hono/jsx/jsx-runtime";
|
|
11
|
+
// src/pages/admin-system-origin.tsx
|
|
12
|
+
const SOURCE_LABELS = {
|
|
13
|
+
config: "configured (PUBLIC_BASE_URL)",
|
|
14
|
+
platform: "detected from the hosting platform",
|
|
15
|
+
admin: "confirmed by an admin"
|
|
16
|
+
};
|
|
17
|
+
/** Where absolute links in emails, commit statuses and signed Storybook URLs point (ADR 0007). */
|
|
18
|
+
function PublicOriginCard(props) {
|
|
19
|
+
const { resolved, stored, learns } = props.origin;
|
|
20
|
+
const fixed = resolved !== null && resolved.source !== "admin";
|
|
21
|
+
return /* @__PURE__ */ jsxs(Card, { children: [
|
|
22
|
+
/* @__PURE__ */ jsx(SectionTitle, { children: "Public origin" }),
|
|
23
|
+
/* @__PURE__ */ jsxs(Meta, { children: [
|
|
24
|
+
"The address links in emails, commit statuses and signed Storybook URLs use. It comes from",
|
|
25
|
+
" ",
|
|
26
|
+
/* @__PURE__ */ jsx("code", { children: "PUBLIC_BASE_URL" }),
|
|
27
|
+
", then the hosting platform, then an address an admin confirmed here. An address detected from a browser is used for none of these until it is confirmed; sign-in (OAuth redirects) may use the address of the request itself."
|
|
28
|
+
] }),
|
|
29
|
+
resolved === null ? /* @__PURE__ */ jsx(NothingResolved, {
|
|
30
|
+
learns,
|
|
31
|
+
stored
|
|
32
|
+
}) : /* @__PURE__ */ jsx(Resolved, { resolved }),
|
|
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
|
+
})
|
|
38
|
+
] });
|
|
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
|
+
}
|
|
44
|
+
function NothingResolved(props) {
|
|
45
|
+
if (props.stored !== null && !props.stored.confirmed) return /* @__PURE__ */ jsxs(Alert, {
|
|
46
|
+
tone: "warning",
|
|
47
|
+
title: "Public origin detected, not confirmed",
|
|
48
|
+
children: [/* @__PURE__ */ jsx("code", { children: props.stored.origin }), " was detected from your browser. Confirm it to enable notifications and signed Storybook URLs: until then no commit status or email is sent, and browsers that reach the Storybook by URL stay unavailable."]
|
|
49
|
+
});
|
|
50
|
+
return /* @__PURE__ */ jsxs(Alert, {
|
|
51
|
+
tone: "warning",
|
|
52
|
+
title: "No public origin is known",
|
|
53
|
+
children: [
|
|
54
|
+
"Notifications are paused, and browsers that reach the Storybook by URL stay unavailable.",
|
|
55
|
+
" ",
|
|
56
|
+
props.learns ? "Reload this page: it reports the address you opened it with, for you to confirm." : "Set PUBLIC_BASE_URL, or turn authentication on so an admin's browser can report it."
|
|
57
|
+
]
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
function Resolved(props) {
|
|
61
|
+
const { resolved } = props;
|
|
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
|
+
] });
|
|
70
|
+
}
|
|
71
|
+
/** Confirm the detected origin, replace it, or drop it. */
|
|
72
|
+
function StoredForm(props) {
|
|
73
|
+
const { stored } = props;
|
|
74
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("form", {
|
|
75
|
+
method: "post",
|
|
76
|
+
action: "/admin/public-origin",
|
|
77
|
+
"hx-post": "/admin/public-origin",
|
|
78
|
+
"hx-target": "body",
|
|
79
|
+
children: [
|
|
80
|
+
csrfField(),
|
|
81
|
+
/* @__PURE__ */ jsx(Field, {
|
|
82
|
+
label: "Stored origin",
|
|
83
|
+
name: "origin",
|
|
84
|
+
value: stored.origin,
|
|
85
|
+
hint: stored.confirmed ? "Confirmed. Change it if this is not the address people use." : "Detected from your browser. Confirm it, or enter the address people use."
|
|
86
|
+
}),
|
|
87
|
+
/* @__PURE__ */ jsx(FormActions, { children: /* @__PURE__ */ jsx(Button, {
|
|
88
|
+
type: "submit",
|
|
89
|
+
variant: "secondary",
|
|
90
|
+
size: "sm",
|
|
91
|
+
children: stored.confirmed ? "Save" : "Confirm"
|
|
92
|
+
}) })
|
|
93
|
+
]
|
|
94
|
+
}), /* @__PURE__ */ jsxs("form", {
|
|
95
|
+
method: "post",
|
|
96
|
+
action: "/admin/public-origin/clear",
|
|
97
|
+
"hx-post": "/admin/public-origin/clear",
|
|
98
|
+
"hx-target": "body",
|
|
99
|
+
children: [csrfField(), /* @__PURE__ */ jsx(Button, {
|
|
100
|
+
type: "submit",
|
|
101
|
+
variant: "secondary",
|
|
102
|
+
size: "sm",
|
|
103
|
+
children: "Forget stored origin"
|
|
104
|
+
})]
|
|
105
|
+
})] });
|
|
106
|
+
}
|
|
107
|
+
/** Enter an origin by hand when none was reported. */
|
|
108
|
+
function EnterForm() {
|
|
109
|
+
return /* @__PURE__ */ jsxs("form", {
|
|
110
|
+
method: "post",
|
|
111
|
+
action: "/admin/public-origin",
|
|
112
|
+
"hx-post": "/admin/public-origin",
|
|
113
|
+
"hx-target": "body",
|
|
114
|
+
children: [
|
|
115
|
+
csrfField(),
|
|
116
|
+
/* @__PURE__ */ jsx(Field, {
|
|
117
|
+
label: "Origin",
|
|
118
|
+
name: "origin",
|
|
119
|
+
placeholder: "https://shelf.example.com"
|
|
120
|
+
}),
|
|
121
|
+
/* @__PURE__ */ jsx(FormActions, { children: /* @__PURE__ */ jsx(Button, {
|
|
122
|
+
type: "submit",
|
|
123
|
+
variant: "secondary",
|
|
124
|
+
size: "sm",
|
|
125
|
+
children: "Save"
|
|
126
|
+
}) })
|
|
127
|
+
]
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
export { PublicOriginCard };
|
|
131
|
+
|
|
132
|
+
//# sourceMappingURL=admin-system-origin.mjs.map
|
|
@@ -0,0 +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: 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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"}
|