@storyshelf/app 0.6.2 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -4
- package/dist/app-types.d.mts +1 -3
- package/dist/asset-manifest.mjs +13 -5
- package/dist/asset-manifest.mjs.map +1 -1
- package/dist/assets/{htmx.min.js → htmx.mjs} +10 -1
- package/dist/assets/htmx.mjs.map +1 -0
- package/dist/assets/mark.mjs +10 -0
- package/dist/assets/mark.mjs.map +1 -0
- package/dist/boot-tasks.mjs +80 -0
- package/dist/boot-tasks.mjs.map +1 -0
- package/dist/build-state-flags.mjs +18 -0
- package/dist/build-state-flags.mjs.map +1 -0
- package/dist/build-state-setup.mjs +75 -0
- package/dist/build-state-setup.mjs.map +1 -0
- package/dist/capture-alerts.mjs +46 -0
- package/dist/capture-alerts.mjs.map +1 -0
- package/dist/capture-process.mjs +59 -0
- package/dist/capture-process.mjs.map +1 -0
- package/dist/capture-setup.mjs +95 -117
- package/dist/capture-setup.mjs.map +1 -1
- package/dist/connections.mjs +34 -0
- package/dist/connections.mjs.map +1 -0
- package/dist/consumer-setup.mjs +58 -0
- package/dist/consumer-setup.mjs.map +1 -0
- package/dist/git-status.mjs +46 -0
- package/dist/git-status.mjs.map +1 -0
- package/dist/hidden-data.mjs +24 -0
- package/dist/hidden-data.mjs.map +1 -0
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +203 -125
- package/dist/index.mjs.map +1 -1
- package/dist/insights/budget.mjs +49 -0
- package/dist/insights/budget.mjs.map +1 -0
- package/dist/insights/deps.mjs +21 -0
- package/dist/insights/deps.mjs.map +1 -0
- package/dist/insights/errors.mjs +22 -0
- package/dist/insights/errors.mjs.map +1 -0
- package/dist/insights/health-panel.mjs +40 -0
- package/dist/insights/health-panel.mjs.map +1 -0
- package/dist/insights/job.mjs +103 -0
- package/dist/insights/job.mjs.map +1 -0
- package/dist/insights/load-health.mjs +59 -0
- package/dist/insights/load-health.mjs.map +1 -0
- package/dist/insights/load-triage.mjs +98 -0
- package/dist/insights/load-triage.mjs.map +1 -0
- package/dist/insights/panel.mjs +28 -0
- package/dist/insights/panel.mjs.map +1 -0
- package/dist/insights/request.mjs +113 -0
- package/dist/insights/request.mjs.map +1 -0
- package/dist/insights/stale.mjs +21 -0
- package/dist/insights/stale.mjs.map +1 -0
- package/dist/insights/usage-report.mjs +60 -0
- package/dist/insights/usage-report.mjs.map +1 -0
- package/dist/insights/view.mjs +48 -0
- package/dist/insights/view.mjs.map +1 -0
- package/dist/job-canceller.mjs +14 -0
- package/dist/job-canceller.mjs.map +1 -0
- package/dist/job-queue.mjs +51 -0
- package/dist/job-queue.mjs.map +1 -0
- package/dist/jobs-setup.mjs +132 -0
- package/dist/jobs-setup.mjs.map +1 -0
- package/dist/lifecycle.mjs +91 -19
- package/dist/lifecycle.mjs.map +1 -1
- package/dist/middleware/auth-gate.mjs +4 -2
- package/dist/middleware/auth-gate.mjs.map +1 -1
- package/dist/middleware/rate-limit.mjs +41 -33
- package/dist/middleware/rate-limit.mjs.map +1 -1
- package/dist/middleware/request-log.mjs +3 -2
- package/dist/middleware/request-log.mjs.map +1 -1
- package/dist/middleware/require-review.mjs +51 -0
- package/dist/middleware/require-review.mjs.map +1 -0
- package/dist/middleware/require-testing.mjs +41 -0
- package/dist/middleware/require-testing.mjs.map +1 -0
- package/dist/middleware/setup-gate.mjs.map +1 -1
- package/dist/middleware/store-scope.mjs +12 -2
- package/dist/middleware/store-scope.mjs.map +1 -1
- package/dist/notifications-pause.mjs +33 -0
- package/dist/notifications-pause.mjs.map +1 -0
- package/dist/notify.d.mts +2 -1
- package/dist/notify.mjs +78 -39
- package/dist/notify.mjs.map +1 -1
- package/dist/pages/a11y-violations.mjs +116 -0
- package/dist/pages/a11y-violations.mjs.map +1 -0
- package/dist/pages/admin-ai.mjs +78 -0
- package/dist/pages/admin-ai.mjs.map +1 -0
- package/dist/pages/admin-system-origin.mjs +123 -0
- package/dist/pages/admin-system-origin.mjs.map +1 -0
- package/dist/pages/admin-system-targets.mjs +53 -0
- package/dist/pages/admin-system-targets.mjs.map +1 -0
- package/dist/pages/admin-system.mjs +63 -13
- package/dist/pages/admin-system.mjs.map +1 -1
- package/dist/pages/build-detail.mjs +104 -76
- package/dist/pages/build-detail.mjs.map +1 -1
- package/dist/pages/build-diff-actions.mjs +87 -33
- package/dist/pages/build-diff-actions.mjs.map +1 -1
- package/dist/pages/build-diff-comments.mjs +16 -14
- package/dist/pages/build-diff-comments.mjs.map +1 -1
- package/dist/pages/build-diff-environment.mjs +82 -0
- package/dist/pages/build-diff-environment.mjs.map +1 -0
- package/dist/pages/build-diff-header.mjs +22 -4
- package/dist/pages/build-diff-header.mjs.map +1 -1
- package/dist/pages/build-diff-insight.mjs +113 -0
- package/dist/pages/build-diff-insight.mjs.map +1 -0
- package/dist/pages/build-diff-model.mjs +55 -10
- package/dist/pages/build-diff-model.mjs.map +1 -1
- package/dist/pages/build-diff-nav.mjs +28 -12
- package/dist/pages/build-diff-nav.mjs.map +1 -1
- package/dist/pages/build-diff-shortcuts.mjs +5 -1
- package/dist/pages/build-diff-shortcuts.mjs.map +1 -1
- package/dist/pages/build-diff-usage.mjs +63 -0
- package/dist/pages/build-diff-usage.mjs.map +1 -0
- package/dist/pages/build-diff-viewer.mjs +62 -5
- package/dist/pages/build-diff-viewer.mjs.map +1 -1
- package/dist/pages/build-diff.mjs +34 -13
- package/dist/pages/build-diff.mjs.map +1 -1
- package/dist/pages/build-tests.mjs +74 -0
- package/dist/pages/build-tests.mjs.map +1 -0
- package/dist/pages/component-detail.mjs +249 -0
- package/dist/pages/component-detail.mjs.map +1 -0
- package/dist/pages/component-stories.mjs +168 -0
- package/dist/pages/component-stories.mjs.map +1 -0
- package/dist/pages/compute-jobs.mjs +13 -26
- package/dist/pages/compute-jobs.mjs.map +1 -1
- package/dist/pages/design-links.mjs +182 -0
- package/dist/pages/design-links.mjs.map +1 -0
- package/dist/pages/environment-change.mjs +53 -0
- package/dist/pages/environment-change.mjs.map +1 -0
- package/dist/pages/health-panel.mjs +126 -0
- package/dist/pages/health-panel.mjs.map +1 -0
- package/dist/pages/library-grid.mjs +105 -0
- package/dist/pages/library-grid.mjs.map +1 -0
- package/dist/pages/library-model.mjs +47 -0
- package/dist/pages/library-model.mjs.map +1 -0
- package/dist/pages/library-snapshots.mjs +130 -0
- package/dist/pages/library-snapshots.mjs.map +1 -0
- package/dist/pages/library.mjs +112 -218
- package/dist/pages/library.mjs.map +1 -1
- package/dist/pages/notifications-paused.mjs +22 -0
- package/dist/pages/notifications-paused.mjs.map +1 -0
- package/dist/pages/profile.mjs +53 -48
- package/dist/pages/profile.mjs.map +1 -1
- package/dist/pages/project-builds-table.mjs +2 -2
- package/dist/pages/project-builds-table.mjs.map +1 -1
- package/dist/pages/project-builds.mjs +51 -21
- package/dist/pages/project-builds.mjs.map +1 -1
- package/dist/pages/project-settings.mjs +51 -12
- package/dist/pages/project-settings.mjs.map +1 -1
- package/dist/pages/projects-card.mjs +14 -1
- package/dist/pages/projects-card.mjs.map +1 -1
- package/dist/pages/projects.mjs +6 -2
- package/dist/pages/projects.mjs.map +1 -1
- package/dist/pages/review-changeset.mjs +116 -0
- package/dist/pages/review-changeset.mjs.map +1 -0
- package/dist/pages/review-comments.mjs +103 -0
- package/dist/pages/review-comments.mjs.map +1 -0
- package/dist/pages/review-verdict.mjs +102 -0
- package/dist/pages/review-verdict.mjs.map +1 -0
- package/dist/pages/settings-ai.mjs +69 -0
- package/dist/pages/settings-ai.mjs.map +1 -0
- package/dist/pages/settings-capture.mjs +124 -0
- package/dist/pages/settings-capture.mjs.map +1 -0
- package/dist/pages/settings-design-credential.mjs +99 -0
- package/dist/pages/settings-design-credential.mjs.map +1 -0
- package/dist/pages/settings-design-links.mjs +179 -0
- package/dist/pages/settings-design-links.mjs.map +1 -0
- package/dist/pages/settings-design.mjs +48 -0
- package/dist/pages/settings-design.mjs.map +1 -0
- package/dist/pages/settings-general.mjs +46 -21
- package/dist/pages/settings-general.mjs.map +1 -1
- package/dist/pages/settings-labels.mjs +43 -2
- package/dist/pages/settings-labels.mjs.map +1 -1
- package/dist/pages/settings-links.mjs +112 -0
- package/dist/pages/settings-links.mjs.map +1 -0
- package/dist/pages/settings-notifications-git.mjs +128 -0
- package/dist/pages/settings-notifications-git.mjs.map +1 -0
- package/dist/pages/settings-notifications.mjs +79 -42
- package/dist/pages/settings-notifications.mjs.map +1 -1
- package/dist/pages/settings-tests.mjs +33 -2
- package/dist/pages/settings-tests.mjs.map +1 -1
- package/dist/pages/target-choices.mjs +39 -0
- package/dist/pages/target-choices.mjs.map +1 -0
- package/dist/publish-setup.mjs +89 -0
- package/dist/publish-setup.mjs.map +1 -0
- package/dist/retention-timer.mjs +2 -1
- package/dist/retention-timer.mjs.map +1 -1
- package/dist/review-events.mjs +48 -0
- package/dist/review-events.mjs.map +1 -0
- package/dist/routers/admin-pages.mjs +45 -3
- package/dist/routers/admin-pages.mjs.map +1 -1
- package/dist/routers/admin-public-origin.mjs +85 -0
- package/dist/routers/admin-public-origin.mjs.map +1 -0
- package/dist/routers/assets.mjs +9 -2
- package/dist/routers/assets.mjs.map +1 -1
- package/dist/routers/auth.mjs +1 -1
- package/dist/routers/baseline-guard.mjs +2 -2
- package/dist/routers/baseline-guard.mjs.map +1 -1
- package/dist/routers/baselines.handlers.mjs +64 -0
- package/dist/routers/baselines.handlers.mjs.map +1 -0
- package/dist/routers/baselines.mjs +125 -0
- package/dist/routers/baselines.mjs.map +1 -0
- package/dist/routers/build-cancel.mjs +68 -0
- package/dist/routers/build-cancel.mjs.map +1 -0
- package/dist/routers/build-changeset.mjs +70 -0
- package/dist/routers/build-changeset.mjs.map +1 -0
- package/dist/routers/build-reviews.handlers.mjs +86 -0
- package/dist/routers/build-reviews.handlers.mjs.map +1 -0
- package/dist/routers/build-reviews.mjs +74 -0
- package/dist/routers/build-reviews.mjs.map +1 -0
- package/dist/routers/builds.handlers.mjs +46 -55
- package/dist/routers/builds.handlers.mjs.map +1 -1
- package/dist/routers/builds.mjs +71 -17
- package/dist/routers/builds.mjs.map +1 -1
- package/dist/routers/capture-config.mjs +35 -0
- package/dist/routers/capture-config.mjs.map +1 -0
- package/dist/routers/comments.mjs +41 -15
- package/dist/routers/comments.mjs.map +1 -1
- package/dist/routers/component-catalog.handlers.mjs +71 -0
- package/dist/routers/component-catalog.handlers.mjs.map +1 -0
- package/dist/routers/component.handlers.mjs +121 -0
- package/dist/routers/component.handlers.mjs.map +1 -0
- package/dist/routers/connections.mjs +118 -0
- package/dist/routers/connections.mjs.map +1 -0
- package/dist/routers/design-connect.handlers.mjs +44 -0
- package/dist/routers/design-connect.handlers.mjs.map +1 -0
- package/dist/routers/design.handlers.mjs +114 -0
- package/dist/routers/design.handlers.mjs.map +1 -0
- package/dist/routers/health-report.mjs +2 -3
- package/dist/routers/health-report.mjs.map +1 -1
- package/dist/routers/health.mjs +4 -3
- package/dist/routers/health.mjs.map +1 -1
- package/dist/routers/helpers.mjs +19 -3
- package/dist/routers/helpers.mjs.map +1 -1
- package/dist/routers/insights-ui.mjs +64 -0
- package/dist/routers/insights-ui.mjs.map +1 -0
- package/dist/routers/insights.handlers.mjs +45 -0
- package/dist/routers/insights.handlers.mjs.map +1 -0
- package/dist/routers/insights.mjs +189 -0
- package/dist/routers/insights.mjs.map +1 -0
- package/dist/routers/links.handlers.mjs +80 -0
- package/dist/routers/links.handlers.mjs.map +1 -0
- package/dist/routers/links.mjs +139 -0
- package/dist/routers/links.mjs.map +1 -0
- package/dist/routers/media.mjs +18 -4
- package/dist/routers/media.mjs.map +1 -1
- package/dist/routers/members.mjs +2 -1
- package/dist/routers/members.mjs.map +1 -1
- package/dist/routers/notification-channels.mjs +17 -23
- package/dist/routers/notification-channels.mjs.map +1 -1
- package/dist/routers/oauth-state.mjs +45 -0
- package/dist/routers/oauth-state.mjs.map +1 -0
- package/dist/routers/preview-base.mjs +38 -0
- package/dist/routers/preview-base.mjs.map +1 -0
- package/dist/routers/preview-token.mjs +51 -0
- package/dist/routers/preview-token.mjs.map +1 -0
- package/dist/routers/profile.mjs +3 -1
- package/dist/routers/profile.mjs.map +1 -1
- package/dist/routers/projects.mjs +17 -1
- package/dist/routers/projects.mjs.map +1 -1
- package/dist/routers/publish-upload.mjs +78 -0
- package/dist/routers/publish-upload.mjs.map +1 -0
- package/dist/routers/published-access.mjs +52 -0
- package/dist/routers/published-access.mjs.map +1 -0
- package/dist/routers/published-files.mjs +52 -0
- package/dist/routers/published-files.mjs.map +1 -0
- package/dist/routers/review-forms.mjs +72 -0
- package/dist/routers/review-forms.mjs.map +1 -0
- package/dist/routers/review-section.handlers.mjs +85 -0
- package/dist/routers/review-section.handlers.mjs.map +1 -0
- package/dist/routers/review-switch-off.mjs +29 -0
- package/dist/routers/review-switch-off.mjs.map +1 -0
- package/dist/routers/schemas.mjs +140 -22
- package/dist/routers/schemas.mjs.map +1 -1
- package/dist/routers/scoped-models.mjs +22 -0
- package/dist/routers/scoped-models.mjs.map +1 -0
- package/dist/routers/settings-ai.mjs +35 -0
- package/dist/routers/settings-ai.mjs.map +1 -0
- package/dist/routers/settings-capture.mjs +77 -0
- package/dist/routers/settings-capture.mjs.map +1 -0
- package/dist/routers/settings-design.mjs +103 -0
- package/dist/routers/settings-design.mjs.map +1 -0
- package/dist/routers/settings-general.mjs +55 -10
- package/dist/routers/settings-general.mjs.map +1 -1
- package/dist/routers/settings-git-channel.mjs +77 -0
- package/dist/routers/settings-git-channel.mjs.map +1 -0
- package/dist/routers/settings-labels.mjs +27 -1
- package/dist/routers/settings-labels.mjs.map +1 -1
- package/dist/routers/settings-links.mjs +63 -0
- package/dist/routers/settings-links.mjs.map +1 -0
- package/dist/routers/settings-members.mjs +4 -7
- package/dist/routers/settings-members.mjs.map +1 -1
- package/dist/routers/settings-notifications.mjs +15 -10
- package/dist/routers/settings-notifications.mjs.map +1 -1
- package/dist/routers/settings-tokens.mjs +2 -5
- package/dist/routers/settings-tokens.mjs.map +1 -1
- package/dist/routers/settings-webhooks.mjs +7 -24
- package/dist/routers/settings-webhooks.mjs.map +1 -1
- package/dist/routers/settings.handlers.mjs +107 -26
- package/dist/routers/settings.handlers.mjs.map +1 -1
- package/dist/routers/settings.mjs +16 -4
- package/dist/routers/settings.mjs.map +1 -1
- package/dist/routers/signed-storybook.mjs +53 -0
- package/dist/routers/signed-storybook.mjs.map +1 -0
- package/dist/routers/snapshot-groups.mjs +63 -0
- package/dist/routers/snapshot-groups.mjs.map +1 -0
- package/dist/routers/snapshots.mjs +103 -5
- package/dist/routers/snapshots.mjs.map +1 -1
- package/dist/routers/storybook.mjs +37 -88
- package/dist/routers/storybook.mjs.map +1 -1
- package/dist/routers/tokens.mjs +2 -5
- package/dist/routers/tokens.mjs.map +1 -1
- package/dist/routers/ui.mjs +34 -5
- package/dist/routers/ui.mjs.map +1 -1
- package/dist/routers/webhooks.mjs +5 -11
- package/dist/routers/webhooks.mjs.map +1 -1
- package/dist/routers/well-known.mjs +11 -4
- package/dist/routers/well-known.mjs.map +1 -1
- package/dist/routes/api-routes.mjs +93 -0
- package/dist/routes/api-routes.mjs.map +1 -0
- package/dist/routes/classification.mjs +38 -0
- package/dist/routes/classification.mjs.map +1 -0
- package/dist/routes/openapi-classes.mjs +22 -0
- package/dist/routes/openapi-classes.mjs.map +1 -0
- package/dist/routes/page-routes.mjs +104 -0
- package/dist/routes/page-routes.mjs.map +1 -0
- package/dist/runtime.mjs +18 -13
- package/dist/runtime.mjs.map +1 -1
- package/dist/serverless.mjs +5 -0
- package/dist/store.mjs +19 -1
- package/dist/store.mjs.map +1 -1
- package/dist/targets.mjs +46 -0
- package/dist/targets.mjs.map +1 -0
- package/dist/ui/buttons.mjs +16 -3
- package/dist/ui/buttons.mjs.map +1 -1
- package/dist/ui/design-reference.mjs +97 -0
- package/dist/ui/design-reference.mjs.map +1 -0
- package/dist/ui/document.mjs +1 -2
- package/dist/ui/document.mjs.map +1 -1
- package/dist/ui/forms.mjs +6 -5
- package/dist/ui/forms.mjs.map +1 -1
- package/dist/ui/icons/paths.mjs +1 -0
- package/dist/ui/icons/paths.mjs.map +1 -1
- package/dist/ui/preview-embed.mjs +82 -0
- package/dist/ui/preview-embed.mjs.map +1 -0
- package/dist/ui/shell/client-script.mjs +85 -0
- package/dist/ui/shell/client-script.mjs.map +1 -1
- package/dist/ui/shell/nav.mjs +12 -8
- package/dist/ui/shell/nav.mjs.map +1 -1
- package/dist/ui/shell/review-script.mjs +8 -0
- package/dist/ui/shell/review-script.mjs.map +1 -1
- package/dist/ui/shell/sidebar.mjs +2 -2
- package/dist/ui/shell/sidebar.mjs.map +1 -1
- package/package.json +11 -9
- 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.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,116 @@
|
|
|
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
|
+
//#region 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
|
+
//#endregion
|
|
114
|
+
export { A11yViolationsDetails, A11yViolationsNotice, a11yCarrierOf, a11yCountOf, a11yFindingsOf, a11ySummaryNote, a11ySummaryOf, a11yUnavailableNote, a11yUnavailableReasonOf, a11yViolationsOf, isAxeUnavailable };
|
|
115
|
+
|
|
116
|
+
//# 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,78 @@
|
|
|
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
|
+
//#region 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
|
+
//#endregion
|
|
76
|
+
export { renderAdminAiPage };
|
|
77
|
+
|
|
78
|
+
//# 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,123 @@
|
|
|
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
|
+
//#region 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 reports = learns && (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
|
+
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
|
|
36
|
+
] });
|
|
37
|
+
}
|
|
38
|
+
function NothingResolved(props) {
|
|
39
|
+
if (props.stored !== null && !props.stored.confirmed) return /* @__PURE__ */ jsxs(Alert, {
|
|
40
|
+
tone: "warning",
|
|
41
|
+
title: "Public origin detected, not confirmed",
|
|
42
|
+
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."]
|
|
43
|
+
});
|
|
44
|
+
return /* @__PURE__ */ jsxs(Alert, {
|
|
45
|
+
tone: "warning",
|
|
46
|
+
title: "No public origin is known",
|
|
47
|
+
children: [
|
|
48
|
+
"Notifications are paused, and browsers that reach the Storybook by URL stay unavailable.",
|
|
49
|
+
" ",
|
|
50
|
+
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."
|
|
51
|
+
]
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
function Resolved(props) {
|
|
55
|
+
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
|
+
})] });
|
|
60
|
+
}
|
|
61
|
+
/** Confirm the detected origin, replace it, or drop it. */
|
|
62
|
+
function StoredForm(props) {
|
|
63
|
+
const { stored } = props;
|
|
64
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("form", {
|
|
65
|
+
method: "post",
|
|
66
|
+
action: "/admin/public-origin",
|
|
67
|
+
"hx-post": "/admin/public-origin",
|
|
68
|
+
"hx-target": "body",
|
|
69
|
+
children: [
|
|
70
|
+
csrfField(),
|
|
71
|
+
/* @__PURE__ */ jsx(Field, {
|
|
72
|
+
label: "Stored origin",
|
|
73
|
+
name: "origin",
|
|
74
|
+
value: stored.origin,
|
|
75
|
+
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."
|
|
76
|
+
}),
|
|
77
|
+
/* @__PURE__ */ jsx(FormActions, { children: /* @__PURE__ */ jsx(Button, {
|
|
78
|
+
type: "submit",
|
|
79
|
+
variant: "secondary",
|
|
80
|
+
size: "sm",
|
|
81
|
+
children: stored.confirmed ? "Save" : "Confirm"
|
|
82
|
+
}) })
|
|
83
|
+
]
|
|
84
|
+
}), /* @__PURE__ */ jsxs("form", {
|
|
85
|
+
method: "post",
|
|
86
|
+
action: "/admin/public-origin/clear",
|
|
87
|
+
"hx-post": "/admin/public-origin/clear",
|
|
88
|
+
"hx-target": "body",
|
|
89
|
+
children: [csrfField(), /* @__PURE__ */ jsx(Button, {
|
|
90
|
+
type: "submit",
|
|
91
|
+
variant: "secondary",
|
|
92
|
+
size: "sm",
|
|
93
|
+
children: "Forget stored origin"
|
|
94
|
+
})]
|
|
95
|
+
})] });
|
|
96
|
+
}
|
|
97
|
+
/** Enter an origin by hand when none was reported. */
|
|
98
|
+
function EnterForm() {
|
|
99
|
+
return /* @__PURE__ */ jsxs("form", {
|
|
100
|
+
method: "post",
|
|
101
|
+
action: "/admin/public-origin",
|
|
102
|
+
"hx-post": "/admin/public-origin",
|
|
103
|
+
"hx-target": "body",
|
|
104
|
+
children: [
|
|
105
|
+
csrfField(),
|
|
106
|
+
/* @__PURE__ */ jsx(Field, {
|
|
107
|
+
label: "Origin",
|
|
108
|
+
name: "origin",
|
|
109
|
+
placeholder: "https://shelf.example.com"
|
|
110
|
+
}),
|
|
111
|
+
/* @__PURE__ */ jsx(FormActions, { children: /* @__PURE__ */ jsx(Button, {
|
|
112
|
+
type: "submit",
|
|
113
|
+
variant: "secondary",
|
|
114
|
+
size: "sm",
|
|
115
|
+
children: "Save"
|
|
116
|
+
}) })
|
|
117
|
+
]
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
//#endregion
|
|
121
|
+
export { PublicOriginCard };
|
|
122
|
+
|
|
123
|
+
//# 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, 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"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { Badge, Meta } from "../ui/feedback.mjs";
|
|
5
|
+
import { Card, SectionTitle } from "../ui/layout.mjs";
|
|
6
|
+
import { Table } from "../ui/table.mjs";
|
|
7
|
+
import { targetLabel } from "../targets.mjs";
|
|
8
|
+
import { jsx, jsxs } from "hono/jsx/jsx-runtime";
|
|
9
|
+
import { unavailableReason } from "@storyshelf/core/adapter/capture-runner";
|
|
10
|
+
//#region src/pages/admin-system-targets.tsx
|
|
11
|
+
/** The browsers the capture runner offers and whether each can render now (ADR 0007, decision 7). */
|
|
12
|
+
function CaptureTargetsCard(props) {
|
|
13
|
+
const { targets } = props;
|
|
14
|
+
return /* @__PURE__ */ jsxs(Card, { children: [
|
|
15
|
+
/* @__PURE__ */ jsx(SectionTitle, { children: "Capture browsers" }),
|
|
16
|
+
/* @__PURE__ */ jsx(Meta, { children: "What the capture runner offers, default first. A project can only pick a target that is ready; a declared engine that is not installed is listed with the reason." }),
|
|
17
|
+
/* @__PURE__ */ jsx(Table, { children: /* @__PURE__ */ jsxs("table", { children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
|
|
18
|
+
/* @__PURE__ */ jsx("th", { children: "Target" }),
|
|
19
|
+
/* @__PURE__ */ jsx("th", { children: "Reaches Storybook" }),
|
|
20
|
+
/* @__PURE__ */ jsx("th", { children: "Status" })
|
|
21
|
+
] }) }), /* @__PURE__ */ jsx("tbody", { children: targets.map((entry, index) => {
|
|
22
|
+
const reason = unavailableReason(entry.status);
|
|
23
|
+
return /* @__PURE__ */ jsxs("tr", { children: [
|
|
24
|
+
/* @__PURE__ */ jsxs("td", { children: [
|
|
25
|
+
/* @__PURE__ */ jsx("strong", { children: targetLabel(entry.target) }),
|
|
26
|
+
" ",
|
|
27
|
+
/* @__PURE__ */ jsx(Meta, {
|
|
28
|
+
as: "span",
|
|
29
|
+
mono: true,
|
|
30
|
+
children: entry.target
|
|
31
|
+
}),
|
|
32
|
+
index === 0 ? /* @__PURE__ */ jsx(Badge, { children: "default" }) : null
|
|
33
|
+
] }),
|
|
34
|
+
/* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx(Meta, {
|
|
35
|
+
as: "span",
|
|
36
|
+
children: entry.reach === "url" ? "signed URL" : "local server"
|
|
37
|
+
}) }),
|
|
38
|
+
/* @__PURE__ */ jsxs("td", { children: [/* @__PURE__ */ jsx(Badge, {
|
|
39
|
+
tone: reason === null ? "success" : "danger",
|
|
40
|
+
icon: true,
|
|
41
|
+
children: reason === null ? "ready" : "unavailable"
|
|
42
|
+
}), reason === null ? null : /* @__PURE__ */ jsx(Meta, {
|
|
43
|
+
as: "div",
|
|
44
|
+
children: reason
|
|
45
|
+
})] })
|
|
46
|
+
] }, entry.target);
|
|
47
|
+
}) })] }) })
|
|
48
|
+
] });
|
|
49
|
+
}
|
|
50
|
+
//#endregion
|
|
51
|
+
export { CaptureTargetsCard };
|
|
52
|
+
|
|
53
|
+
//# sourceMappingURL=admin-system-targets.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admin-system-targets.mjs","names":[],"sources":["../../src/pages/admin-system-targets.tsx"],"sourcesContent":["import { unavailableReason, type RunnerTarget } from \"@storyshelf/core/adapter/capture-runner\";\nimport type { HtmlEscapedString } from \"hono/utils/html\";\nimport { targetLabel } from \"../targets.ts\";\nimport { Badge, Card, Meta, SectionTitle, Table } from \"../ui/components.tsx\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\n/** The browsers the capture runner offers and whether each can render now (ADR 0007, decision 7). */\nexport function CaptureTargetsCard(props: {\n targets: readonly RunnerTarget[];\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { targets } = props;\n return (\n <Card>\n <SectionTitle>Capture browsers</SectionTitle>\n <Meta>\n What the capture runner offers, default first. A project can only pick a target that is\n ready; a declared engine that is not installed is listed with the reason.\n </Meta>\n <Table>\n <table>\n <thead>\n <tr>\n <th>Target</th>\n <th>Reaches Storybook</th>\n <th>Status</th>\n </tr>\n </thead>\n <tbody>\n {targets.map((entry, index) => {\n const reason = unavailableReason(entry.status);\n return (\n <tr key={entry.target}>\n <td>\n <strong>{targetLabel(entry.target)}</strong>{\" \"}\n <Meta as=\"span\" mono>\n {entry.target}\n </Meta>\n {index === 0 ? <Badge>default</Badge> : null}\n </td>\n <td>\n <Meta as=\"span\">{entry.reach === \"url\" ? \"signed URL\" : \"local server\"}</Meta>\n </td>\n <td>\n <Badge tone={reason === null ? \"success\" : \"danger\"} icon>\n {reason === null ? \"ready\" : \"unavailable\"}\n </Badge>\n {reason === null ? null : <Meta as=\"div\">{reason}</Meta>}\n </td>\n </tr>\n );\n })}\n </tbody>\n </table>\n </Table>\n </Card>\n );\n}\n"],"mappings":";;;;;;;;;;;AAQA,SAAgB,mBAAmB,OAEgB;CACjD,MAAM,EAAE,YAAY;CACpB,OACE,qBAAC,MAAD,EAAA,UAAA;EACE,oBAAC,cAAD,EAAA,UAAc,mBAA8B,CAAA;EAC5C,oBAAC,MAAD,EAAA,UAAM,oKAGA,CAAA;EACN,oBAAC,OAAD,EAAA,UACE,qBAAC,SAAD,EAAA,UAAA,CACE,oBAAC,SAAD,EAAA,UACE,qBAAC,MAAD,EAAA,UAAA;GACE,oBAAC,MAAD,EAAA,UAAI,SAAU,CAAA;GACd,oBAAC,MAAD,EAAA,UAAI,oBAAqB,CAAA;GACzB,oBAAC,MAAD,EAAA,UAAI,SAAU,CAAA;EACZ,EAAA,CAAA,EACC,CAAA,GACP,oBAAC,SAAD,EAAA,UACG,QAAQ,KAAK,OAAO,UAAU;GAC7B,MAAM,SAAS,kBAAkB,MAAM,MAAM;GAC7C,OACE,qBAAC,MAAD,EAAA,UAAA;IACE,qBAAC,MAAD,EAAA,UAAA;KACE,oBAAC,UAAD,EAAA,UAAS,YAAY,MAAM,MAAM,EAAU,CAAA;KAAE;KAC7C,oBAAC,MAAD;MAAM,IAAG;MAAO,MAAA;MACb,UAAA,MAAM;KACH,CAAA;KACL,UAAU,IAAI,oBAAC,OAAD,EAAA,UAAO,UAAc,CAAA,IAAI;IACtC,EAAA,CAAA;IACJ,oBAAC,MAAD,EAAA,UACE,oBAAC,MAAD;KAAM,IAAG;KAAQ,UAAA,MAAM,UAAU,QAAQ,eAAe;IAAqB,CAAA,EAC3E,CAAA;IACJ,qBAAC,MAAD,EAAA,UAAA,CACE,oBAAC,OAAD;KAAO,MAAM,WAAW,OAAO,YAAY;KAAU,MAAA;KAClD,UAAA,WAAW,OAAO,UAAU;IACxB,CAAA,GACN,WAAW,OAAO,OAAO,oBAAC,MAAD;KAAM,IAAG;KAAO,UAAA;IAAa,CAAA,CACrD,EAAA,CAAA;GACF,EAAA,GAjBK,MAAM,MAiBX;EAER,CAAC,EACI,CAAA,CACF,EAAA,CAAA,EACF,CAAA;CACH,EAAA,CAAA;AAEV"}
|
|
@@ -8,6 +8,9 @@ import { HStack } from "../ui/stacks.mjs";
|
|
|
8
8
|
import { Table } from "../ui/table.mjs";
|
|
9
9
|
import { csrfField } from "../ui/csrf-field.mjs";
|
|
10
10
|
import { DocumentLayout } from "../ui/document.mjs";
|
|
11
|
+
import { PublicOriginCard } from "./admin-system-origin.mjs";
|
|
12
|
+
import { CaptureTargetsCard } from "./admin-system-targets.mjs";
|
|
13
|
+
import { NotificationsPausedNotice } from "./notifications-paused.mjs";
|
|
11
14
|
import { Fragment, jsx, jsxs } from "hono/jsx/jsx-runtime";
|
|
12
15
|
//#region src/pages/admin-system.tsx
|
|
13
16
|
function healthTone(state) {
|
|
@@ -90,9 +93,27 @@ function AdapterTable(props) {
|
|
|
90
93
|
/* @__PURE__ */ jsx("th", { children: "Detail" })
|
|
91
94
|
] }) }), /* @__PURE__ */ jsx("tbody", { children: report.adapters.map((adapter) => /* @__PURE__ */ jsx(AdapterRow, { adapter }, `${adapter.category}/${adapter.kind}`)) })] }) });
|
|
92
95
|
}
|
|
96
|
+
/** Detected hardware and the capture concurrency it yields (auto unless configured). */
|
|
97
|
+
function CaptureSizing(props) {
|
|
98
|
+
const { cpus, memoryMb, builds, pages } = props.capture;
|
|
99
|
+
return /* @__PURE__ */ jsxs(Meta, { children: [
|
|
100
|
+
"Capture host ",
|
|
101
|
+
cpus,
|
|
102
|
+
" CPU · ",
|
|
103
|
+
memoryMb,
|
|
104
|
+
" MB · builds at once ",
|
|
105
|
+
builds.value,
|
|
106
|
+
builds.auto ? " (auto)" : " (configured)",
|
|
107
|
+
" · pages per build ",
|
|
108
|
+
pages.start,
|
|
109
|
+
"–",
|
|
110
|
+
pages.max,
|
|
111
|
+
" (auto, adapts to load unless the runner pins it)"
|
|
112
|
+
] });
|
|
113
|
+
}
|
|
93
114
|
/** Non-secret server facts (never secret, admin token, or credentials). */
|
|
94
115
|
function ServerFacts(props) {
|
|
95
|
-
const { report, authEnabled, config, authMethods } = props;
|
|
116
|
+
const { report, authEnabled, config, authMethods, capture } = props.data;
|
|
96
117
|
return /* @__PURE__ */ jsxs(Card, { children: [
|
|
97
118
|
/* @__PURE__ */ jsx(SectionTitle, { children: "Server" }),
|
|
98
119
|
/* @__PURE__ */ jsxs("div", {
|
|
@@ -115,9 +136,6 @@ function ServerFacts(props) {
|
|
|
115
136
|
/* @__PURE__ */ jsxs(Meta, { children: [
|
|
116
137
|
"Auth ",
|
|
117
138
|
authEnabled ? "enabled" : "disabled — all operations permitted",
|
|
118
|
-
" · Capture concurrency",
|
|
119
|
-
" ",
|
|
120
|
-
formatOptional(config.captureConcurrency),
|
|
121
139
|
" · Purge TTL",
|
|
122
140
|
" ",
|
|
123
141
|
formatOptional(config.purgeTtlDays),
|
|
@@ -126,6 +144,7 @@ function ServerFacts(props) {
|
|
|
126
144
|
config.branchTtlDays === null ? "disabled" : `${formatOptional(config.branchTtlDays)} days`
|
|
127
145
|
] }),
|
|
128
146
|
authMethods && authMethods.length > 0 ? /* @__PURE__ */ jsx(HStack, { children: authMethods.map((method) => /* @__PURE__ */ jsx(Badge, { children: method }, method)) }) : null,
|
|
147
|
+
capture ? /* @__PURE__ */ jsx(CaptureSizing, { capture }) : null,
|
|
129
148
|
/* @__PURE__ */ jsxs(Meta, { children: [
|
|
130
149
|
"Scratch dir ",
|
|
131
150
|
formatOptional(config.scratchDir),
|
|
@@ -188,9 +207,37 @@ function CredentialsCard(props) {
|
|
|
188
207
|
}) : /* @__PURE__ */ jsx(Meta, { children: rotating ? "All credentials use the current secret. Remove SECRET_PREVIOUS." : "To rotate, set SECRET to the new value and SECRET_PREVIOUS to the old one." })
|
|
189
208
|
] });
|
|
190
209
|
}
|
|
210
|
+
/** Notice that `testing` is off, so a misconfigured instance in a fleet is visible (ADR 0006). */
|
|
211
|
+
function HostingOnlyNotice(props) {
|
|
212
|
+
const { hiddenSnapshots } = props;
|
|
213
|
+
return /* @__PURE__ */ jsx("div", {
|
|
214
|
+
class: "mt-1",
|
|
215
|
+
children: /* @__PURE__ */ jsxs(Alert, {
|
|
216
|
+
tone: "info",
|
|
217
|
+
title: "Testing is off: Storybook hosting only",
|
|
218
|
+
children: [
|
|
219
|
+
"This instance publishes and serves Storybooks, but does not capture, compare or keep baselines, and no testing status is posted. Set the ",
|
|
220
|
+
/* @__PURE__ */ jsx("code", { children: "testing" }),
|
|
221
|
+
" option to turn testing on.",
|
|
222
|
+
hiddenSnapshots > 0 ? ` ${hiddenSnapshots} existing snapshot(s) are hidden, not deleted, and reappear when testing is turned on.` : ""
|
|
223
|
+
]
|
|
224
|
+
})
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
/** The public origin and the capture browsers (ADR 0007), when the instance has them. */
|
|
228
|
+
function CaptureCards(props) {
|
|
229
|
+
const { publicOrigin, targets } = props.data;
|
|
230
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [publicOrigin ? /* @__PURE__ */ jsx("div", {
|
|
231
|
+
class: "mt-1",
|
|
232
|
+
children: /* @__PURE__ */ jsx(PublicOriginCard, { origin: publicOrigin })
|
|
233
|
+
}) : null, targets && targets.length > 0 ? /* @__PURE__ */ jsx("div", {
|
|
234
|
+
class: "mt-1",
|
|
235
|
+
children: /* @__PURE__ */ jsx(CaptureTargetsCard, { targets })
|
|
236
|
+
}) : null] });
|
|
237
|
+
}
|
|
191
238
|
/** Site-admin System page: adapter inventory with in-depth health. */
|
|
192
239
|
function renderAdminSystemPage(data) {
|
|
193
|
-
const { report,
|
|
240
|
+
const { report, config, credentials } = data;
|
|
194
241
|
return /* @__PURE__ */ jsxs(DocumentLayout, {
|
|
195
242
|
title: "System",
|
|
196
243
|
nav: { active: "admin" },
|
|
@@ -206,19 +253,22 @@ function renderAdminSystemPage(data) {
|
|
|
206
253
|
" ",
|
|
207
254
|
formatUptime(report.uptimeSecs)
|
|
208
255
|
] }),
|
|
209
|
-
actions: /* @__PURE__ */ jsx(Button, {
|
|
256
|
+
actions: /* @__PURE__ */ jsxs(HStack, { children: [data.aiEnabled ? /* @__PURE__ */ jsx(Button, {
|
|
257
|
+
variant: "secondary",
|
|
258
|
+
size: "sm",
|
|
259
|
+
href: "/admin/ai",
|
|
260
|
+
children: "AI usage"
|
|
261
|
+
}) : null, /* @__PURE__ */ jsx(Button, {
|
|
210
262
|
variant: "secondary",
|
|
211
263
|
size: "sm",
|
|
212
264
|
href: "/admin",
|
|
213
265
|
children: "Recheck"
|
|
214
|
-
})
|
|
215
|
-
}),
|
|
216
|
-
/* @__PURE__ */ jsx(ServerFacts, {
|
|
217
|
-
report,
|
|
218
|
-
authEnabled,
|
|
219
|
-
config,
|
|
220
|
-
authMethods
|
|
266
|
+
})] })
|
|
221
267
|
}),
|
|
268
|
+
/* @__PURE__ */ jsx(NotificationsPausedNotice, { paused: data.notificationsPaused }),
|
|
269
|
+
data.testing === false ? /* @__PURE__ */ jsx(HostingOnlyNotice, { hiddenSnapshots: data.hiddenSnapshots ?? 0 }) : null,
|
|
270
|
+
/* @__PURE__ */ jsx(ServerFacts, { data }),
|
|
271
|
+
/* @__PURE__ */ jsx(CaptureCards, { data }),
|
|
222
272
|
credentials ? /* @__PURE__ */ jsx("div", {
|
|
223
273
|
class: "mt-1",
|
|
224
274
|
children: /* @__PURE__ */ jsx(CredentialsCard, {
|