@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
|
@@ -3,14 +3,20 @@ import "node:url";
|
|
|
3
3
|
import.meta.url;
|
|
4
4
|
import { getStore } from "../store.mjs";
|
|
5
5
|
import { Button } from "../ui/buttons.mjs";
|
|
6
|
-
import {
|
|
6
|
+
import { Badge, EmptyState, Meta, statusTone } from "../ui/feedback.mjs";
|
|
7
7
|
import { Card, PageHeader, SectionTitle } from "../ui/layout.mjs";
|
|
8
8
|
import { HStack } from "../ui/stacks.mjs";
|
|
9
9
|
import { Table } from "../ui/table.mjs";
|
|
10
10
|
import { DocumentLayout } from "../ui/document.mjs";
|
|
11
11
|
import { BuildModel, ProjectModel } from "@storyshelf/core/models";
|
|
12
12
|
import { jsx, jsxs } from "hono/jsx/jsx-runtime";
|
|
13
|
-
|
|
13
|
+
// src/pages/compute-jobs.tsx
|
|
14
|
+
/** Build statuses the retry button of the jobs table offers. */
|
|
15
|
+
const RETRYABLE = /* @__PURE__ */ new Set([
|
|
16
|
+
"failed",
|
|
17
|
+
"cancelled",
|
|
18
|
+
"pending"
|
|
19
|
+
]);
|
|
14
20
|
/** Live-refreshing partial showing the currently queued and running captures. */
|
|
15
21
|
function renderActiveQueue(slug, queueView) {
|
|
16
22
|
return /* @__PURE__ */ jsxs(Card, {
|
|
@@ -44,21 +50,12 @@ function renderActiveQueue(slug, queueView) {
|
|
|
44
50
|
] }, job.buildId)) })] }) })]
|
|
45
51
|
});
|
|
46
52
|
}
|
|
47
|
-
/**
|
|
48
|
-
* A `capturing` build the in-process queue no longer tracks was cut short by a
|
|
49
|
-
* restart. Remote queues are invisible to the server, so nothing is flagged there.
|
|
50
|
-
*/
|
|
51
|
-
function isStuckCapture(status, tracked, inProcessQueue) {
|
|
52
|
-
return inProcessQueue && status === "capturing" && !tracked;
|
|
53
|
-
}
|
|
54
53
|
/** Compute jobs page: live capture queue plus recent build history with retry. */
|
|
55
|
-
async function renderComputeJobsPage(slug, queueView, canRetry
|
|
54
|
+
async function renderComputeJobsPage(slug, queueView, canRetry) {
|
|
56
55
|
const project = (await new ProjectModel(getStore().db).list()).find((item) => item.slug === slug);
|
|
57
56
|
if (!project) return null;
|
|
58
57
|
const recentBuilds = (await new BuildModel(getStore().db).list(project.id)).slice(0, 20);
|
|
59
58
|
const queueByBuild = new Map(queueView.map((job) => [job.buildId, job]));
|
|
60
|
-
const stuck = (id, status) => isStuckCapture(status, queueByBuild.has(id), inProcessQueue);
|
|
61
|
-
const stuckCount = recentBuilds.filter((build) => stuck(build.id, build.status)).length;
|
|
62
59
|
return /* @__PURE__ */ jsxs(DocumentLayout, {
|
|
63
60
|
title: `${project.name} · Compute jobs`,
|
|
64
61
|
nav: {
|
|
@@ -76,23 +73,13 @@ async function renderComputeJobsPage(slug, queueView, canRetry, inProcessQueue =
|
|
|
76
73
|
children: "Back to builds"
|
|
77
74
|
})
|
|
78
75
|
}),
|
|
79
|
-
stuckCount > 0 ? /* @__PURE__ */ jsxs(Alert, {
|
|
80
|
-
tone: "warning",
|
|
81
|
-
title: "Interrupted captures",
|
|
82
|
-
children: [
|
|
83
|
-
stuckCount,
|
|
84
|
-
" build",
|
|
85
|
-
stuckCount === 1 ? " was" : "s were",
|
|
86
|
-
" still capturing when the server restarted. On startup an interrupted build is requeued once; if it is interrupted again it is marked failed so a crash loop cannot repeat captures. Retry a build to run it again."
|
|
87
|
-
]
|
|
88
|
-
}) : null,
|
|
89
76
|
renderActiveQueue(project.slug, queueView),
|
|
90
77
|
/* @__PURE__ */ jsxs(Card, { children: [
|
|
91
78
|
/* @__PURE__ */ jsx(SectionTitle, { children: "Recent builds" }),
|
|
92
79
|
/* @__PURE__ */ jsxs(Meta, { children: [
|
|
93
80
|
"Capture history for ",
|
|
94
81
|
project.name,
|
|
95
|
-
". Failed
|
|
82
|
+
". Failed jobs can be retried."
|
|
96
83
|
] }),
|
|
97
84
|
/* @__PURE__ */ jsx(Table, { children: /* @__PURE__ */ jsxs("table", { children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
|
|
98
85
|
/* @__PURE__ */ jsx("th", { children: "Branch" }),
|
|
@@ -110,8 +97,8 @@ async function renderComputeJobsPage(slug, queueView, canRetry, inProcessQueue =
|
|
|
110
97
|
children: build.gitSha.slice(0, 7)
|
|
111
98
|
})] }),
|
|
112
99
|
/* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx(Badge, {
|
|
113
|
-
tone:
|
|
114
|
-
children:
|
|
100
|
+
tone: statusTone(build.status),
|
|
101
|
+
children: build.status
|
|
115
102
|
}) }),
|
|
116
103
|
/* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsxs(Meta, {
|
|
117
104
|
as: "span",
|
|
@@ -135,7 +122,7 @@ async function renderComputeJobsPage(slug, queueView, canRetry, inProcessQueue =
|
|
|
135
122
|
size: "sm",
|
|
136
123
|
href: `/projects/${project.slug}/builds/${build.id}`,
|
|
137
124
|
children: "View"
|
|
138
|
-
}), canRetry &&
|
|
125
|
+
}), canRetry && RETRYABLE.has(build.status) ? /* @__PURE__ */ jsx("form", {
|
|
139
126
|
method: "post",
|
|
140
127
|
action: `/api/v1/projects/${project.slug}/builds/${build.id}/retry`,
|
|
141
128
|
"hx-post": `/api/v1/projects/${project.slug}/builds/${build.id}/retry`,
|
|
@@ -159,7 +146,6 @@ async function renderComputeJobsPage(slug, queueView, canRetry, inProcessQueue =
|
|
|
159
146
|
]
|
|
160
147
|
});
|
|
161
148
|
}
|
|
162
|
-
|
|
163
|
-
export { isStuckCapture, renderActiveQueue, renderComputeJobsPage };
|
|
149
|
+
export { renderActiveQueue, renderComputeJobsPage };
|
|
164
150
|
|
|
165
151
|
//# sourceMappingURL=compute-jobs.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compute-jobs.mjs","names":[],"sources":["../../src/pages/compute-jobs.tsx"],"sourcesContent":["import { BuildModel } from \"@storyshelf/core/models\";\nimport { ProjectModel } from \"@storyshelf/core/models\";\nimport type { HtmlEscapedString } from \"hono/utils/html\";\nimport { getStore } from \"../store.ts\";\nimport {\n Alert,\n Badge,\n Button,\n Card,\n EmptyState,\n HStack,\n Meta,\n PageHeader,\n SectionTitle,\n Table,\n statusTone,\n} from \"../ui/components.tsx\";\nimport { DocumentLayout, type RenderedContent } from \"../ui/document.tsx\";\ninterface QueueView {\n buildId: string;\n status: string;\n queuedAt: string;\n startedAt?: string;\n finishedAt?: string;\n error?: string;\n}\n\n/** Live-refreshing partial showing the currently queued and running captures. */\nexport function renderActiveQueue(slug: string, queueView: QueueView[]): RenderedContent {\n return (\n <Card\n id=\"active-queue\"\n hx-get={`/projects/${slug}/jobs?partial=queue`}\n hx-trigger=\"every 5s\"\n hx-swap=\"outerHTML\"\n hx-target=\"#active-queue\"\n >\n <SectionTitle>Active queue</SectionTitle>\n {queueView.length === 0 ? (\n <Meta>No captures are currently queued or running.</Meta>\n ) : (\n <Table>\n <table>\n <thead>\n <tr>\n <th>Build</th>\n <th>Status</th>\n <th>Queued</th>\n <th>Started</th>\n </tr>\n </thead>\n <tbody>\n {queueView.map((job): HtmlEscapedString | Promise<HtmlEscapedString> => (\n <tr key={job.buildId}>\n <td>\n <a href={`/projects/${slug}/builds/${job.buildId}`}>\n {job.buildId.slice(0, 8)}\n </a>\n </td>\n <td>\n <Badge tone={statusTone(job.status)}>{job.status}</Badge>\n </td>\n <td>\n <Meta as=\"span\">{new Date(job.queuedAt).toLocaleTimeString()}</Meta>\n </td>\n <td>\n <Meta as=\"span\">\n {job.startedAt ? new Date(job.startedAt).toLocaleTimeString() : \"—\"}\n </Meta>\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </Table>\n )}\n </Card>\n );\n}\n\n/**\n * A `capturing` build the in-process queue no longer tracks was cut short by a\n * restart. Remote queues are invisible to the server, so nothing is flagged there.\n */\nexport function isStuckCapture(status: string, tracked: boolean, inProcessQueue: boolean): boolean {\n return inProcessQueue && status === \"capturing\" && !tracked;\n}\n\n/** Compute jobs page: live capture queue plus recent build history with retry. */\nexport async function renderComputeJobsPage(\n slug: string,\n queueView: QueueView[],\n canRetry: boolean,\n inProcessQueue = false,\n): Promise<RenderedContent | null> {\n const projects = await new ProjectModel(getStore().db).list();\n const project = projects.find((item) => item.slug === slug);\n if (!project) {\n return null;\n }\n const builds = await new BuildModel(getStore().db).list(project.id);\n const recentBuilds = builds.slice(0, 20);\n const queueByBuild = new Map(queueView.map((job) => [job.buildId, job]));\n const stuck = (id: string, status: string): boolean =>\n isStuckCapture(status, queueByBuild.has(id), inProcessQueue);\n const stuckCount = recentBuilds.filter((build) => stuck(build.id, build.status)).length;\n\n return (\n <DocumentLayout\n title={`${project.name} · Compute jobs`}\n nav={{ active: \"jobs\", projectSlug: project.slug, projectName: project.name }}\n >\n <PageHeader\n title=\"Compute jobs\"\n description=\"Capture jobs run on this server. Queued and running jobs refresh live; recent history is below.\"\n actions={\n <Button variant=\"secondary\" href={`/projects/${project.slug}/builds`}>\n Back to builds\n </Button>\n }\n />\n\n {stuckCount > 0 ? (\n <Alert tone=\"warning\" title=\"Interrupted captures\">\n {stuckCount} build{stuckCount === 1 ? \" was\" : \"s were\"} still capturing when the server\n restarted. On startup an interrupted build is requeued once; if it is interrupted again it\n is marked failed so a crash loop cannot repeat captures. Retry a build to run it again.\n </Alert>\n ) : null}\n\n {renderActiveQueue(project.slug, queueView)}\n\n <Card>\n <SectionTitle>Recent builds</SectionTitle>\n <Meta>Capture history for {project.name}. Failed and interrupted jobs can be retried.</Meta>\n <Table>\n <table>\n <thead>\n <tr>\n <th>Branch</th>\n <th>Status</th>\n <th>Snapshots</th>\n <th>Created</th>\n <th>\n <span class=\"visually-hidden\">Actions</span>\n </th>\n </tr>\n </thead>\n <tbody>\n {recentBuilds.map((build): HtmlEscapedString | Promise<HtmlEscapedString> => (\n <tr key={build.id}>\n <td>\n <strong>{build.gitBranch}</strong>\n <Meta as=\"div\" mono>\n {build.gitSha.slice(0, 7)}\n </Meta>\n </td>\n <td>\n <Badge\n tone={stuck(build.id, build.status) ? \"warning\" : statusTone(build.status)}\n >\n {stuck(build.id, build.status) ? \"interrupted\" : build.status}\n </Badge>\n </td>\n <td>\n <Meta as=\"span\">\n {build.snapshotCount} total · {build.changedCount} changed\n </Meta>\n </td>\n <td>\n <Meta as=\"span\">{new Date(build.createdAt).toLocaleString()}</Meta>\n </td>\n <td class=\"nowrap\">\n <HStack wrap={false}>\n <Button\n variant=\"secondary\"\n size=\"sm\"\n href={`/projects/${project.slug}/builds/${build.id}`}\n >\n View\n </Button>\n {canRetry &&\n (build.status === \"failed\" ||\n build.status === \"pending\" ||\n stuck(build.id, build.status)) ? (\n <form\n method=\"post\"\n action={`/api/v1/projects/${project.slug}/builds/${build.id}/retry`}\n hx-post={`/api/v1/projects/${project.slug}/builds/${build.id}/retry`}\n hx-target=\"body\"\n >\n <Button variant=\"ghost\" size=\"sm\" type=\"submit\">\n Retry\n </Button>\n </form>\n ) : null}\n </HStack>\n {queueByBuild.get(build.id)?.error ? (\n <Meta as=\"div\" tone=\"danger\">\n {queueByBuild.get(build.id)?.error}\n </Meta>\n ) : null}\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </Table>\n {recentBuilds.length === 0 ? <EmptyState description=\"No builds yet.\" /> : null}\n </Card>\n </DocumentLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"compute-jobs.mjs","names":[],"sources":["../../src/pages/compute-jobs.tsx"],"sourcesContent":["import { BuildModel } from \"@storyshelf/core/models\";\nimport { ProjectModel } from \"@storyshelf/core/models\";\nimport type { HtmlEscapedString } from \"hono/utils/html\";\nimport { getStore } from \"../store.ts\";\nimport {\n Badge,\n Button,\n Card,\n EmptyState,\n HStack,\n Meta,\n PageHeader,\n SectionTitle,\n Table,\n statusTone,\n} from \"../ui/components.tsx\";\nimport { DocumentLayout, type RenderedContent } from \"../ui/document.tsx\";\n\n/** Build statuses the retry button of the jobs table offers. */\nconst RETRYABLE: ReadonlySet<string> = new Set([\"failed\", \"cancelled\", \"pending\"]);\n\ninterface QueueView {\n buildId: string;\n status: string;\n queuedAt: string;\n startedAt?: string;\n finishedAt?: string;\n error?: string;\n}\n\n/** Live-refreshing partial showing the currently queued and running captures. */\nexport function renderActiveQueue(slug: string, queueView: QueueView[]): RenderedContent {\n return (\n <Card\n id=\"active-queue\"\n hx-get={`/projects/${slug}/jobs?partial=queue`}\n hx-trigger=\"every 5s\"\n hx-swap=\"outerHTML\"\n hx-target=\"#active-queue\"\n >\n <SectionTitle>Active queue</SectionTitle>\n {queueView.length === 0 ? (\n <Meta>No captures are currently queued or running.</Meta>\n ) : (\n <Table>\n <table>\n <thead>\n <tr>\n <th>Build</th>\n <th>Status</th>\n <th>Queued</th>\n <th>Started</th>\n </tr>\n </thead>\n <tbody>\n {queueView.map((job): HtmlEscapedString | Promise<HtmlEscapedString> => (\n <tr key={job.buildId}>\n <td>\n <a href={`/projects/${slug}/builds/${job.buildId}`}>\n {job.buildId.slice(0, 8)}\n </a>\n </td>\n <td>\n <Badge tone={statusTone(job.status)}>{job.status}</Badge>\n </td>\n <td>\n <Meta as=\"span\">{new Date(job.queuedAt).toLocaleTimeString()}</Meta>\n </td>\n <td>\n <Meta as=\"span\">\n {job.startedAt ? new Date(job.startedAt).toLocaleTimeString() : \"—\"}\n </Meta>\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </Table>\n )}\n </Card>\n );\n}\n\n/** Compute jobs page: live capture queue plus recent build history with retry. */\nexport async function renderComputeJobsPage(\n slug: string,\n queueView: QueueView[],\n canRetry: boolean,\n): Promise<RenderedContent | null> {\n const projects = await new ProjectModel(getStore().db).list();\n const project = projects.find((item) => item.slug === slug);\n if (!project) {\n return null;\n }\n const builds = await new BuildModel(getStore().db).list(project.id);\n const recentBuilds = builds.slice(0, 20);\n const queueByBuild = new Map(queueView.map((job) => [job.buildId, job]));\n\n return (\n <DocumentLayout\n title={`${project.name} · Compute jobs`}\n nav={{ active: \"jobs\", projectSlug: project.slug, projectName: project.name }}\n >\n <PageHeader\n title=\"Compute jobs\"\n description=\"Capture jobs run on this server. Queued and running jobs refresh live; recent history is below.\"\n actions={\n <Button variant=\"secondary\" href={`/projects/${project.slug}/builds`}>\n Back to builds\n </Button>\n }\n />\n\n {renderActiveQueue(project.slug, queueView)}\n\n <Card>\n <SectionTitle>Recent builds</SectionTitle>\n <Meta>Capture history for {project.name}. Failed jobs can be retried.</Meta>\n <Table>\n <table>\n <thead>\n <tr>\n <th>Branch</th>\n <th>Status</th>\n <th>Snapshots</th>\n <th>Created</th>\n <th>\n <span class=\"visually-hidden\">Actions</span>\n </th>\n </tr>\n </thead>\n <tbody>\n {recentBuilds.map((build): HtmlEscapedString | Promise<HtmlEscapedString> => (\n <tr key={build.id}>\n <td>\n <strong>{build.gitBranch}</strong>\n <Meta as=\"div\" mono>\n {build.gitSha.slice(0, 7)}\n </Meta>\n </td>\n <td>\n <Badge tone={statusTone(build.status)}>{build.status}</Badge>\n </td>\n <td>\n <Meta as=\"span\">\n {build.snapshotCount} total · {build.changedCount} changed\n </Meta>\n </td>\n <td>\n <Meta as=\"span\">{new Date(build.createdAt).toLocaleString()}</Meta>\n </td>\n <td class=\"nowrap\">\n <HStack wrap={false}>\n <Button\n variant=\"secondary\"\n size=\"sm\"\n href={`/projects/${project.slug}/builds/${build.id}`}\n >\n View\n </Button>\n {canRetry && RETRYABLE.has(build.status) ? (\n <form\n method=\"post\"\n action={`/api/v1/projects/${project.slug}/builds/${build.id}/retry`}\n hx-post={`/api/v1/projects/${project.slug}/builds/${build.id}/retry`}\n hx-target=\"body\"\n >\n <Button variant=\"ghost\" size=\"sm\" type=\"submit\">\n Retry\n </Button>\n </form>\n ) : null}\n </HStack>\n {queueByBuild.get(build.id)?.error ? (\n <Meta as=\"div\" tone=\"danger\">\n {queueByBuild.get(build.id)?.error}\n </Meta>\n ) : null}\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </Table>\n {recentBuilds.length === 0 ? <EmptyState description=\"No builds yet.\" /> : null}\n </Card>\n </DocumentLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { Button } from "../ui/buttons.mjs";
|
|
5
|
+
import { Alert, Meta } from "../ui/feedback.mjs";
|
|
6
|
+
import { HStack } from "../ui/stacks.mjs";
|
|
7
|
+
import { Dropdown, DropdownItem } from "../ui/dropdown.mjs";
|
|
8
|
+
import { DesignPreview, DesignReference } from "../ui/design-reference.mjs";
|
|
9
|
+
import { csrfField } from "../ui/csrf-field.mjs";
|
|
10
|
+
import { Fragment, jsx, jsxs } from "hono/jsx/jsx-runtime";
|
|
11
|
+
// src/pages/design-links.tsx
|
|
12
|
+
/**
|
|
13
|
+
* Design actions for a component's link(s). A link with a cached render opens a
|
|
14
|
+
* click popover (preview + `Open in <provider>`); others are plain deep links.
|
|
15
|
+
*/
|
|
16
|
+
function DesignButtons(props) {
|
|
17
|
+
const { urls, providerName, previews, componentHref, size = "sm" } = props;
|
|
18
|
+
return /* @__PURE__ */ jsx(Fragment, { children: urls.map((url, index) => {
|
|
19
|
+
const name = providerName(url);
|
|
20
|
+
const label = urls.length > 1 ? `${name} ${index + 1}` : name;
|
|
21
|
+
const preview = previews?.get(url);
|
|
22
|
+
return preview?.renderUrl ? /* @__PURE__ */ jsxs(Dropdown, {
|
|
23
|
+
label,
|
|
24
|
+
children: [
|
|
25
|
+
/* @__PURE__ */ jsx(DesignPreview, {
|
|
26
|
+
src: preview.renderUrl,
|
|
27
|
+
name: preview.name,
|
|
28
|
+
providerName: name
|
|
29
|
+
}),
|
|
30
|
+
/* @__PURE__ */ jsxs(DropdownItem, {
|
|
31
|
+
href: url,
|
|
32
|
+
icon: "external-link",
|
|
33
|
+
target: "_blank",
|
|
34
|
+
rel: "noopener noreferrer",
|
|
35
|
+
children: ["Open in ", name]
|
|
36
|
+
}),
|
|
37
|
+
componentHref ? /* @__PURE__ */ jsx(DropdownItem, {
|
|
38
|
+
href: componentHref,
|
|
39
|
+
children: "View component →"
|
|
40
|
+
}) : null
|
|
41
|
+
]
|
|
42
|
+
}, url) : /* @__PURE__ */ jsx(Button, {
|
|
43
|
+
variant: "ghost",
|
|
44
|
+
size,
|
|
45
|
+
icon: "external-link",
|
|
46
|
+
href: url,
|
|
47
|
+
target: "_blank",
|
|
48
|
+
rel: "noopener noreferrer",
|
|
49
|
+
children: label
|
|
50
|
+
}, url);
|
|
51
|
+
}) });
|
|
52
|
+
}
|
|
53
|
+
/** Reference panes (cached renders) for the review page, shown under the diff stage. */
|
|
54
|
+
function DesignReferences(props) {
|
|
55
|
+
return /* @__PURE__ */ jsx(Fragment, { children: props.items.map((item) => item.renderUrl ? /* @__PURE__ */ jsx(DesignReference, {
|
|
56
|
+
src: item.renderUrl,
|
|
57
|
+
name: item.name,
|
|
58
|
+
href: item.url,
|
|
59
|
+
providerName: item.providerName
|
|
60
|
+
}, item.url) : null) });
|
|
61
|
+
}
|
|
62
|
+
/** Review-page design strip: open-in-provider actions plus non-blocking stale/deleted notices. */
|
|
63
|
+
function DesignStrip(props) {
|
|
64
|
+
const { items } = props;
|
|
65
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
66
|
+
/* @__PURE__ */ jsx(HStack, { children: items.map((item) => /* @__PURE__ */ jsxs(HStack, {
|
|
67
|
+
wrap: false,
|
|
68
|
+
children: [/* @__PURE__ */ jsxs(Button, {
|
|
69
|
+
variant: "secondary",
|
|
70
|
+
size: "sm",
|
|
71
|
+
icon: "external-link",
|
|
72
|
+
href: item.url,
|
|
73
|
+
target: "_blank",
|
|
74
|
+
rel: "noopener noreferrer",
|
|
75
|
+
children: ["Open in ", item.providerName]
|
|
76
|
+
}), item.name ? /* @__PURE__ */ jsx(Meta, {
|
|
77
|
+
as: "span",
|
|
78
|
+
children: item.name
|
|
79
|
+
}) : null]
|
|
80
|
+
}, item.url)) }),
|
|
81
|
+
items.map((item) => designNotice(item)),
|
|
82
|
+
connectPrompts(items)
|
|
83
|
+
] });
|
|
84
|
+
}
|
|
85
|
+
/** One connect/disconnect prompt per provider (several links of one provider share it). */
|
|
86
|
+
function connectPrompts(items) {
|
|
87
|
+
return [...new Map(items.flatMap((item) => item.connect ? [[item.providerName, item]] : [])).values()].map((item) => connectPrompt(item, item.connect));
|
|
88
|
+
}
|
|
89
|
+
function connectPrompt(item, connect) {
|
|
90
|
+
if (!connect) return /* @__PURE__ */ jsx(Fragment, {});
|
|
91
|
+
if (connect.state === "connected") return /* @__PURE__ */ jsxs("form", {
|
|
92
|
+
method: "post",
|
|
93
|
+
action: connect.disconnectUrl,
|
|
94
|
+
children: [
|
|
95
|
+
csrfField(),
|
|
96
|
+
/* @__PURE__ */ jsx("input", {
|
|
97
|
+
type: "hidden",
|
|
98
|
+
name: "return",
|
|
99
|
+
value: connect.returnTo
|
|
100
|
+
}),
|
|
101
|
+
/* @__PURE__ */ jsxs(HStack, {
|
|
102
|
+
wrap: false,
|
|
103
|
+
children: [/* @__PURE__ */ jsxs(Meta, {
|
|
104
|
+
as: "span",
|
|
105
|
+
children: [
|
|
106
|
+
"Previews use your ",
|
|
107
|
+
item.providerName,
|
|
108
|
+
" account."
|
|
109
|
+
]
|
|
110
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
111
|
+
variant: "ghost",
|
|
112
|
+
size: "sm",
|
|
113
|
+
type: "submit",
|
|
114
|
+
children: "Disconnect"
|
|
115
|
+
})]
|
|
116
|
+
})
|
|
117
|
+
]
|
|
118
|
+
}, item.providerName);
|
|
119
|
+
return /* @__PURE__ */ jsxs(Alert, {
|
|
120
|
+
tone: connect.state === "reconnect" ? "warning" : "info",
|
|
121
|
+
title: connect.state === "reconnect" ? `Your ${item.providerName} connection expired` : `Connect ${item.providerName} for design previews`,
|
|
122
|
+
children: [
|
|
123
|
+
"Sign in with your own ",
|
|
124
|
+
item.providerName,
|
|
125
|
+
" account to see names, previews and stale-design warnings. Only you will see what your account can open.",
|
|
126
|
+
" ",
|
|
127
|
+
/* @__PURE__ */ jsxs(Button, {
|
|
128
|
+
variant: "secondary",
|
|
129
|
+
size: "sm",
|
|
130
|
+
href: connect.startUrl,
|
|
131
|
+
children: [
|
|
132
|
+
connect.state === "reconnect" ? "Reconnect" : "Connect",
|
|
133
|
+
" ",
|
|
134
|
+
item.providerName
|
|
135
|
+
]
|
|
136
|
+
})
|
|
137
|
+
]
|
|
138
|
+
}, item.providerName);
|
|
139
|
+
}
|
|
140
|
+
function designNotice(item) {
|
|
141
|
+
if (item.state === "deleted") return /* @__PURE__ */ jsxs(Alert, {
|
|
142
|
+
tone: "warning",
|
|
143
|
+
title: "Design not found",
|
|
144
|
+
children: [
|
|
145
|
+
"The linked ",
|
|
146
|
+
item.providerName,
|
|
147
|
+
" file or node no longer exists. Update the link in the project's Design settings."
|
|
148
|
+
]
|
|
149
|
+
}, item.url);
|
|
150
|
+
if (item.stale) return /* @__PURE__ */ jsxs(Alert, {
|
|
151
|
+
tone: "warning",
|
|
152
|
+
title: "Design changed since this baseline was approved",
|
|
153
|
+
children: [
|
|
154
|
+
item.name ? `${item.name} was` : "The linked design was",
|
|
155
|
+
" edited in ",
|
|
156
|
+
item.providerName,
|
|
157
|
+
" ",
|
|
158
|
+
"after the baseline. This does not block review."
|
|
159
|
+
]
|
|
160
|
+
}, item.url);
|
|
161
|
+
if (item.state === "unauthorized" || item.state === "forbidden") return /* @__PURE__ */ jsx(Meta, {
|
|
162
|
+
as: "div",
|
|
163
|
+
children: item.state === "unauthorized" ? `The project's ${item.providerName} credential is expired or revoked — design status is unavailable.` : `The project's ${item.providerName} credential cannot open this file — design status is unavailable.`
|
|
164
|
+
}, item.url);
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
/** Library header links to the project's design files. */
|
|
168
|
+
function ProjectDesignLinks(props) {
|
|
169
|
+
return /* @__PURE__ */ jsx(HStack, { children: props.links.map((link) => /* @__PURE__ */ jsx(Button, {
|
|
170
|
+
variant: "ghost",
|
|
171
|
+
size: "sm",
|
|
172
|
+
icon: "external-link",
|
|
173
|
+
href: link.url,
|
|
174
|
+
target: "_blank",
|
|
175
|
+
rel: "noopener noreferrer",
|
|
176
|
+
children: link.label
|
|
177
|
+
}, link.url)) });
|
|
178
|
+
}
|
|
179
|
+
export { DesignButtons, DesignReferences, DesignStrip, ProjectDesignLinks };
|
|
180
|
+
|
|
181
|
+
//# sourceMappingURL=design-links.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"design-links.mjs","names":[],"sources":["../../src/pages/design-links.tsx"],"sourcesContent":["import type { HtmlEscapedString } from \"hono/utils/html\";\nimport {\n Alert,\n Button,\n DesignPreview,\n DesignReference,\n Dropdown,\n DropdownItem,\n HStack,\n Meta,\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\ntype Rendered = HtmlEscapedString | Promise<HtmlEscapedString>;\n\n/** \"Connect your own account\" state for a provider whose project has no shared credential. */\nexport interface DesignConnect {\n /** `connect` (not connected), `reconnect` (connection expired) or `connected`. */\n state: \"connect\" | \"reconnect\" | \"connected\";\n startUrl: string;\n disconnectUrl: string;\n returnTo: string;\n}\n\n/** A design link a component resolves to, with cached provider metadata when available. */\nexport interface DesignItem {\n url: string;\n /** Display name of the provider that owns the link (\"Figma\"). */\n providerName: string;\n /** Design node/file name from the metadata cache. */\n name: string | null;\n /** Cache state: `ok` | `deleted` | `unauthorized` | `forbidden` | `unknown` (nothing cached). */\n state: string;\n /** API URL of the cached render, when one exists. */\n renderUrl: string | null;\n /** The design changed after the baseline it is compared with was approved. */\n stale: boolean;\n /** Offer to connect the viewer's own account; absent when not applicable. */\n connect?: DesignConnect;\n}\n\n/** Cached name and render of a design link, for library popovers. */\nexport interface DesignPreviewInfo {\n name: string | null;\n renderUrl: string | null;\n}\n\n/** A project-level design file link for the library header. */\nexport interface ProjectDesignLink {\n label: string;\n url: string;\n}\n\n/**\n * Design actions for a component's link(s). A link with a cached render opens a\n * click popover (preview + `Open in <provider>`); others are plain deep links.\n */\nexport function DesignButtons(props: {\n urls: readonly string[];\n /** Display name of the provider that owns a link. */\n providerName: (url: string) => string;\n previews?: ReadonlyMap<string, DesignPreviewInfo>;\n /** Component page of the story, offered in the popover as `View component →`. */\n componentHref?: string;\n size?: \"sm\" | \"md\";\n}): Rendered {\n const { urls, providerName, previews, componentHref, size = \"sm\" } = props;\n return (\n <>\n {urls.map((url, index): Rendered => {\n const name = providerName(url);\n const label = urls.length > 1 ? `${name} ${index + 1}` : name;\n const preview = previews?.get(url);\n return preview?.renderUrl ? (\n <Dropdown key={url} label={label}>\n <DesignPreview src={preview.renderUrl} name={preview.name} providerName={name} />\n <DropdownItem href={url} icon=\"external-link\" target=\"_blank\" rel=\"noopener noreferrer\">\n Open in {name}\n </DropdownItem>\n {componentHref ? (\n <DropdownItem href={componentHref}>View component →</DropdownItem>\n ) : null}\n </Dropdown>\n ) : (\n <Button\n key={url}\n variant=\"ghost\"\n size={size}\n icon=\"external-link\"\n href={url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n {label}\n </Button>\n );\n })}\n </>\n );\n}\n\n/** Reference panes (cached renders) for the review page, shown under the diff stage. */\nexport function DesignReferences(props: { items: DesignItem[] }): Rendered {\n return (\n <>\n {props.items.map((item): Rendered | null =>\n item.renderUrl ? (\n <DesignReference\n key={item.url}\n src={item.renderUrl}\n name={item.name}\n href={item.url}\n providerName={item.providerName}\n />\n ) : null,\n )}\n </>\n );\n}\n\n/** Review-page design strip: open-in-provider actions plus non-blocking stale/deleted notices. */\nexport function DesignStrip(props: { items: DesignItem[] }): Rendered {\n const { items } = props;\n return (\n <>\n <HStack>\n {items.map((item): Rendered => (\n <HStack key={item.url} wrap={false}>\n <Button\n variant=\"secondary\"\n size=\"sm\"\n icon=\"external-link\"\n href={item.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n Open in {item.providerName}\n </Button>\n {item.name ? <Meta as=\"span\">{item.name}</Meta> : null}\n </HStack>\n ))}\n </HStack>\n {items.map((item): Rendered | null => designNotice(item))}\n {connectPrompts(items)}\n </>\n );\n}\n\n/** One connect/disconnect prompt per provider (several links of one provider share it). */\nfunction connectPrompts(items: DesignItem[]): Rendered[] {\n const byProvider = new Map(\n items.flatMap((item) => (item.connect ? [[item.providerName, item] as const] : [])),\n );\n return [...byProvider.values()].map((item): Rendered => connectPrompt(item, item.connect));\n}\n\nfunction connectPrompt(item: DesignItem, connect: DesignConnect | undefined): Rendered {\n if (!connect) return <></>;\n if (connect.state === \"connected\") {\n return (\n <form key={item.providerName} method=\"post\" action={connect.disconnectUrl}>\n {csrfField()}\n <input type=\"hidden\" name=\"return\" value={connect.returnTo} />\n <HStack wrap={false}>\n <Meta as=\"span\">Previews use your {item.providerName} account.</Meta>\n <Button variant=\"ghost\" size=\"sm\" type=\"submit\">\n Disconnect\n </Button>\n </HStack>\n </form>\n );\n }\n return (\n <Alert\n key={item.providerName}\n tone={connect.state === \"reconnect\" ? \"warning\" : \"info\"}\n title={\n connect.state === \"reconnect\"\n ? `Your ${item.providerName} connection expired`\n : `Connect ${item.providerName} for design previews`\n }\n >\n Sign in with your own {item.providerName} account to see names, previews and stale-design\n warnings. Only you will see what your account can open.{\" \"}\n <Button variant=\"secondary\" size=\"sm\" href={connect.startUrl}>\n {connect.state === \"reconnect\" ? \"Reconnect\" : \"Connect\"} {item.providerName}\n </Button>\n </Alert>\n );\n}\n\nfunction designNotice(item: DesignItem): Rendered | null {\n if (item.state === \"deleted\") {\n return (\n <Alert key={item.url} tone=\"warning\" title=\"Design not found\">\n The linked {item.providerName} file or node no longer exists. Update the link in the\n project's Design settings.\n </Alert>\n );\n }\n if (item.stale) {\n return (\n <Alert key={item.url} tone=\"warning\" title=\"Design changed since this baseline was approved\">\n {item.name ? `${item.name} was` : \"The linked design was\"} edited in {item.providerName}{\" \"}\n after the baseline. This does not block review.\n </Alert>\n );\n }\n if (item.state === \"unauthorized\" || item.state === \"forbidden\") {\n return (\n <Meta key={item.url} as=\"div\">\n {item.state === \"unauthorized\"\n ? `The project's ${item.providerName} credential is expired or revoked — design status is unavailable.`\n : `The project's ${item.providerName} credential cannot open this file — design status is unavailable.`}\n </Meta>\n );\n }\n return null;\n}\n\n/** Library header links to the project's design files. */\nexport function ProjectDesignLinks(props: { links: ProjectDesignLink[] }): Rendered {\n return (\n <HStack>\n {props.links.map((link): Rendered => (\n <Button\n key={link.url}\n variant=\"ghost\"\n size=\"sm\"\n icon=\"external-link\"\n href={link.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n {link.label}\n </Button>\n ))}\n </HStack>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
// src/pages/environment-change.ts
|
|
5
|
+
/** What changed, in words a reviewer recognises. */
|
|
6
|
+
const FIELD_LABELS = {
|
|
7
|
+
engine: "browser engine",
|
|
8
|
+
browserVersion: "browser version",
|
|
9
|
+
platform: "host platform",
|
|
10
|
+
deviceScaleFactor: "device scale factor",
|
|
11
|
+
fontsHash: "installed fonts",
|
|
12
|
+
viewport: "viewport size",
|
|
13
|
+
sizing: "screenshot sizing",
|
|
14
|
+
unknown: "capture environment"
|
|
15
|
+
};
|
|
16
|
+
/** A snapshot's recorded change; null when it has none or the stored value is unreadable. */
|
|
17
|
+
function environmentChangeOf(snapshot) {
|
|
18
|
+
if (!snapshot.envChange) return null;
|
|
19
|
+
try {
|
|
20
|
+
const value = JSON.parse(snapshot.envChange);
|
|
21
|
+
return Array.isArray(value.changed) && value.to ? value : null;
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** Summarise the build's environment migrations, or null when there were none. */
|
|
27
|
+
function summariseEnvironmentChanges(snapshots) {
|
|
28
|
+
const changes = snapshots.map((snapshot) => environmentChangeOf(snapshot)).filter((change) => change !== null);
|
|
29
|
+
if (changes.length === 0) return null;
|
|
30
|
+
const fields = [...new Set(changes.flatMap((change) => change.changed))].map((field) => labelOf(field));
|
|
31
|
+
return {
|
|
32
|
+
count: changes.length,
|
|
33
|
+
fields,
|
|
34
|
+
...browserVersionOf(changes)
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/** Human label for a changed component. */
|
|
38
|
+
function labelOf(field) {
|
|
39
|
+
return FIELD_LABELS[field] ?? field;
|
|
40
|
+
}
|
|
41
|
+
function browserVersionOf(changes) {
|
|
42
|
+
const pairs = new Set(changes.map((change) => `${change.from?.browserVersion ?? ""}\u0000${change.to.browserVersion ?? ""}`));
|
|
43
|
+
if (pairs.size !== 1) return {};
|
|
44
|
+
const [from, to] = [...pairs][0]?.split("\0") ?? [];
|
|
45
|
+
return from && to && from !== to ? { browserVersion: {
|
|
46
|
+
from,
|
|
47
|
+
to
|
|
48
|
+
} } : {};
|
|
49
|
+
}
|
|
50
|
+
export { environmentChangeOf, labelOf, summariseEnvironmentChanges };
|
|
51
|
+
|
|
52
|
+
//# sourceMappingURL=environment-change.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"environment-change.mjs","names":[],"sources":["../../src/pages/environment-change.ts"],"sourcesContent":["/** Reads the capture-environment migrations recorded on a build's snapshots. */\nimport type { EnvironmentChange } from \"@storyshelf/core/capture\";\nimport type { Snapshot } from \"@storyshelf/core/schema\";\n\n/** What changed, in words a reviewer recognises. */\nconst FIELD_LABELS: Record<string, string> = {\n engine: \"browser engine\",\n browserVersion: \"browser version\",\n platform: \"host platform\",\n deviceScaleFactor: \"device scale factor\",\n fontsHash: \"installed fonts\",\n viewport: \"viewport size\",\n sizing: \"screenshot sizing\",\n unknown: \"capture environment\",\n};\n\n/** A snapshot's recorded change; null when it has none or the stored value is unreadable. */\nexport function environmentChangeOf(\n snapshot: Pick<Snapshot, \"envChange\">,\n): EnvironmentChange | null {\n if (!snapshot.envChange) {\n return null;\n }\n try {\n const value = JSON.parse(snapshot.envChange) as Partial<EnvironmentChange>;\n return Array.isArray(value.changed) && value.to ? (value as EnvironmentChange) : null;\n } catch {\n return null;\n }\n}\n\n/** Summary across a build's snapshots. */\nexport interface EnvironmentChangeSummary {\n /** Snapshots accepted as a migration. */\n count: number;\n /** Distinct labels of what changed, in first-seen order. */\n fields: string[];\n /** `from -> to` for the browser version when every migrated snapshot agrees on it. */\n browserVersion?: { from: string; to: string };\n}\n\n/** Summarise the build's environment migrations, or null when there were none. */\nexport function summariseEnvironmentChanges(\n snapshots: readonly Pick<Snapshot, \"envChange\">[],\n): EnvironmentChangeSummary | null {\n const changes = snapshots\n .map((snapshot) => environmentChangeOf(snapshot))\n .filter((change): change is EnvironmentChange => change !== null);\n if (changes.length === 0) {\n return null;\n }\n const fields = [...new Set(changes.flatMap((change) => change.changed))].map((field) =>\n labelOf(field),\n );\n return { count: changes.length, fields, ...browserVersionOf(changes) };\n}\n\n/** Human label for a changed component. */\nexport function labelOf(field: string): string {\n return FIELD_LABELS[field] ?? field;\n}\n\nfunction browserVersionOf(\n changes: EnvironmentChange[],\n): Pick<EnvironmentChangeSummary, \"browserVersion\"> {\n const pairs = new Set(\n changes.map(\n (change) => `${change.from?.browserVersion ?? \"\"}\\u0000${change.to.browserVersion ?? \"\"}`,\n ),\n );\n if (pairs.size !== 1) {\n return {};\n }\n const [from, to] = [...pairs][0]?.split(\"\\u0000\") ?? [];\n return from && to && from !== to ? { browserVersion: { from, to } } : {};\n}\n"],"mappings":";;;;;AAKA,MAAM,eAAuC;CAC3C,QAAQ;CACR,gBAAgB;CAChB,UAAU;CACV,mBAAmB;CACnB,WAAW;CACX,UAAU;CACV,QAAQ;CACR,SAAS;AACX;;AAGA,SAAgB,oBACd,UAC0B;CAC1B,IAAI,CAAC,SAAS,WACZ,OAAO;CAET,IAAI;EACF,MAAM,QAAQ,KAAK,MAAM,SAAS,SAAS;EAC3C,OAAO,MAAM,QAAQ,MAAM,OAAO,KAAK,MAAM,KAAM,QAA8B;CACnF,QAAQ;EACN,OAAO;CACT;AACF;;AAaA,SAAgB,4BACd,WACiC;CACjC,MAAM,UAAU,UACb,KAAK,aAAa,oBAAoB,QAAQ,CAAC,CAAC,CAChD,QAAQ,WAAwC,WAAW,IAAI;CAClE,IAAI,QAAQ,WAAW,GACrB,OAAO;CAET,MAAM,SAAS,CAAC,GAAG,IAAI,IAAI,QAAQ,SAAS,WAAW,OAAO,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,UAC5E,QAAQ,KAAK,CACf;CACA,OAAO;EAAE,OAAO,QAAQ;EAAQ;EAAQ,GAAG,iBAAiB,OAAO;CAAE;AACvE;;AAGA,SAAgB,QAAQ,OAAuB;CAC7C,OAAO,aAAa,UAAU;AAChC;AAEA,SAAS,iBACP,SACkD;CAClD,MAAM,QAAQ,IAAI,IAChB,QAAQ,KACL,WAAW,GAAG,OAAO,MAAM,kBAAkB,GAAG,QAAQ,OAAO,GAAG,kBAAkB,IACvF,CACF;CACA,IAAI,MAAM,SAAS,GACjB,OAAO,CAAC;CAEV,MAAM,CAAC,MAAM,MAAM,CAAC,GAAG,KAAK,CAAC,CAAC,EAAE,EAAE,MAAM,IAAQ,KAAK,CAAC;CACtD,OAAO,QAAQ,MAAM,SAAS,KAAK,EAAE,gBAAgB;EAAE;EAAM;CAAG,EAAE,IAAI,CAAC;AACzE"}
|
|
@@ -0,0 +1,125 @@
|
|
|
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 { Card, SectionTitle } from "../ui/layout.mjs";
|
|
7
|
+
import { HStack, VStack } from "../ui/stacks.mjs";
|
|
8
|
+
import { csrfField } from "../ui/csrf-field.mjs";
|
|
9
|
+
import { Fragment, jsx, jsxs } from "hono/jsx/jsx-runtime";
|
|
10
|
+
// src/pages/health-panel.tsx
|
|
11
|
+
/** Badge tone for a health verdict. */
|
|
12
|
+
function healthTone(verdict) {
|
|
13
|
+
if (verdict === "healthy") return "success";
|
|
14
|
+
if (verdict === "watch") return "warning";
|
|
15
|
+
return verdict === "unhealthy" ? "danger" : "neutral";
|
|
16
|
+
}
|
|
17
|
+
function trendsOf(output) {
|
|
18
|
+
const trends = output?.trends;
|
|
19
|
+
return Array.isArray(trends) ? trends : [];
|
|
20
|
+
}
|
|
21
|
+
function scoreText(output) {
|
|
22
|
+
const score = output?.score;
|
|
23
|
+
return typeof score === "number" ? ` · score ${Math.round(score)}/100` : "";
|
|
24
|
+
}
|
|
25
|
+
function HealthBody(props) {
|
|
26
|
+
const { latest } = props;
|
|
27
|
+
if (!latest) return /* @__PURE__ */ jsxs(Meta, { children: [
|
|
28
|
+
"No health digest yet for the last ",
|
|
29
|
+
"30d",
|
|
30
|
+
"."
|
|
31
|
+
] });
|
|
32
|
+
if (latest.status === "failed") return /* @__PURE__ */ jsxs(Alert, {
|
|
33
|
+
tone: "warning",
|
|
34
|
+
title: "Health digest failed",
|
|
35
|
+
children: [
|
|
36
|
+
"The model call did not finish (",
|
|
37
|
+
latest.errorCode,
|
|
38
|
+
"). You can try again."
|
|
39
|
+
]
|
|
40
|
+
});
|
|
41
|
+
if (latest.status !== "done") return /* @__PURE__ */ jsx(Meta, { children: "Working on it…" });
|
|
42
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
43
|
+
/* @__PURE__ */ jsx("span", { children: latest.summary }),
|
|
44
|
+
/* @__PURE__ */ jsx("ul", { children: trendsOf(latest.output).map((trend) => /* @__PURE__ */ jsxs("li", { children: [
|
|
45
|
+
/* @__PURE__ */ jsx("strong", { children: trend.label }),
|
|
46
|
+
" (",
|
|
47
|
+
trend.direction,
|
|
48
|
+
"): ",
|
|
49
|
+
trend.note
|
|
50
|
+
] })) }),
|
|
51
|
+
/* @__PURE__ */ jsxs(Meta, { children: [
|
|
52
|
+
latest.profile,
|
|
53
|
+
" · ",
|
|
54
|
+
latest.model,
|
|
55
|
+
" · ",
|
|
56
|
+
latest.createdAt.slice(0, 16).replace("T", " "),
|
|
57
|
+
scoreText(latest.output)
|
|
58
|
+
] })
|
|
59
|
+
] });
|
|
60
|
+
}
|
|
61
|
+
/** Project health digest (advisory) with a generate/refresh button for approvers and admins. */
|
|
62
|
+
function HealthPanel(props) {
|
|
63
|
+
const { project, data } = props;
|
|
64
|
+
const base = `/projects/${project.slug}/insights/health`;
|
|
65
|
+
const latest = data.latest;
|
|
66
|
+
const inFlight = latest?.status === "pending" || latest?.status === "running";
|
|
67
|
+
const poll = inFlight ? {
|
|
68
|
+
"hx-get": `${base}/panel`,
|
|
69
|
+
"hx-trigger": "load delay:3s",
|
|
70
|
+
"hx-swap": "outerHTML"
|
|
71
|
+
} : {};
|
|
72
|
+
return /* @__PURE__ */ jsx("div", {
|
|
73
|
+
id: "health-panel",
|
|
74
|
+
...poll,
|
|
75
|
+
children: /* @__PURE__ */ jsxs(Card, { children: [/* @__PURE__ */ jsxs(HStack, { children: [
|
|
76
|
+
/* @__PURE__ */ jsxs(SectionTitle, {
|
|
77
|
+
level: 3,
|
|
78
|
+
children: [
|
|
79
|
+
"Project health (AI, last ",
|
|
80
|
+
"30d",
|
|
81
|
+
")"
|
|
82
|
+
]
|
|
83
|
+
}),
|
|
84
|
+
latest?.status === "done" ? /* @__PURE__ */ jsx(Badge, {
|
|
85
|
+
tone: healthTone(latest.verdict),
|
|
86
|
+
children: latest.verdict
|
|
87
|
+
}) : null,
|
|
88
|
+
inFlight ? /* @__PURE__ */ jsx(Badge, {
|
|
89
|
+
tone: "info",
|
|
90
|
+
children: "generating…"
|
|
91
|
+
}) : null
|
|
92
|
+
] }), /* @__PURE__ */ jsxs(VStack, { children: [/* @__PURE__ */ jsx(HealthBody, { latest }), data.canGenerate && !inFlight ? /* @__PURE__ */ jsxs("form", {
|
|
93
|
+
method: "post",
|
|
94
|
+
action: `${base}/generate`,
|
|
95
|
+
"hx-post": `${base}/generate`,
|
|
96
|
+
"hx-target": "#health-panel",
|
|
97
|
+
"hx-swap": "outerHTML",
|
|
98
|
+
children: [
|
|
99
|
+
csrfField(),
|
|
100
|
+
/* @__PURE__ */ jsx("input", {
|
|
101
|
+
type: "hidden",
|
|
102
|
+
name: "force",
|
|
103
|
+
value: latest ? "true" : "false"
|
|
104
|
+
}),
|
|
105
|
+
/* @__PURE__ */ jsx(Button, {
|
|
106
|
+
variant: "secondary",
|
|
107
|
+
size: "sm",
|
|
108
|
+
type: "submit",
|
|
109
|
+
icon: "sparkles",
|
|
110
|
+
children: latest ? "Refresh health" : "Generate health"
|
|
111
|
+
})
|
|
112
|
+
]
|
|
113
|
+
}) : null] })] })
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
/** Renders the panel when AI health is available for the project, nothing otherwise. */
|
|
117
|
+
function HealthSlot(props) {
|
|
118
|
+
return props.data ? /* @__PURE__ */ jsx(HealthPanel, {
|
|
119
|
+
project: props.project,
|
|
120
|
+
data: props.data
|
|
121
|
+
}) : null;
|
|
122
|
+
}
|
|
123
|
+
export { HealthPanel, HealthSlot, healthTone };
|
|
124
|
+
|
|
125
|
+
//# sourceMappingURL=health-panel.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"health-panel.mjs","names":[],"sources":["../../src/pages/health-panel.tsx"],"sourcesContent":["import type { Project } from \"@storyshelf/core/schema\";\nimport type { HtmlEscapedString } from \"hono/utils/html\";\nimport { HEALTH_UI_WINDOW, type HealthPanelData } from \"../insights/health-panel.ts\";\nimport {\n Alert,\n Badge,\n Button,\n Card,\n HStack,\n Meta,\n SectionTitle,\n VStack,\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/** Badge tone for a health verdict. */\nexport function healthTone(verdict: string | null): \"success\" | \"warning\" | \"danger\" | \"neutral\" {\n if (verdict === \"healthy\") {\n return \"success\";\n }\n if (verdict === \"watch\") {\n return \"warning\";\n }\n return verdict === \"unhealthy\" ? \"danger\" : \"neutral\";\n}\n\ninterface Trend {\n label: string;\n note: string;\n direction: string;\n}\n\nfunction trendsOf(output: unknown): Trend[] {\n const trends = (output as { trends?: unknown } | null)?.trends;\n return Array.isArray(trends) ? (trends as Trend[]) : [];\n}\n\nfunction scoreText(output: unknown): string {\n const score = (output as { score?: unknown } | null)?.score;\n return typeof score === \"number\" ? ` · score ${Math.round(score)}/100` : \"\";\n}\n\nfunction HealthBody(props: {\n latest: HealthPanelData[\"latest\"];\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { latest } = props;\n if (!latest) {\n return <Meta>No health digest yet for the last {HEALTH_UI_WINDOW}.</Meta>;\n }\n if (latest.status === \"failed\") {\n return (\n <Alert tone=\"warning\" title=\"Health digest failed\">\n The model call did not finish ({latest.errorCode}). You can try again.\n </Alert>\n );\n }\n if (latest.status !== \"done\") {\n return <Meta>Working on it…</Meta>;\n }\n return (\n <>\n <span>{latest.summary}</span>\n <ul>\n {trendsOf(latest.output).map((trend) => (\n <li>\n <strong>{trend.label}</strong> ({trend.direction}): {trend.note}\n </li>\n ))}\n </ul>\n <Meta>\n {latest.profile} · {latest.model} · {latest.createdAt.slice(0, 16).replace(\"T\", \" \")}\n {scoreText(latest.output)}\n </Meta>\n </>\n );\n}\n\n/** Props for the health panel. */\nexport interface HealthPanelProps {\n project: Project;\n data: HealthPanelData;\n}\n\n/** Project health digest (advisory) with a generate/refresh button for approvers and admins. */\nexport function HealthPanel(\n props: HealthPanelProps,\n): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { project, data } = props;\n const base = `/projects/${project.slug}/insights/health`;\n const latest = data.latest;\n const inFlight = latest?.status === \"pending\" || latest?.status === \"running\";\n const poll = inFlight\n ? { \"hx-get\": `${base}/panel`, \"hx-trigger\": \"load delay:3s\", \"hx-swap\": \"outerHTML\" }\n : {};\n return (\n <div id=\"health-panel\" {...poll}>\n <Card>\n <HStack>\n <SectionTitle level={3}>Project health (AI, last {HEALTH_UI_WINDOW})</SectionTitle>\n {latest?.status === \"done\" ? (\n <Badge tone={healthTone(latest.verdict)}>{latest.verdict}</Badge>\n ) : null}\n {inFlight ? <Badge tone=\"info\">generating…</Badge> : null}\n </HStack>\n <VStack>\n <HealthBody latest={latest} />\n {data.canGenerate && !inFlight ? (\n <form\n method=\"post\"\n action={`${base}/generate`}\n hx-post={`${base}/generate`}\n hx-target=\"#health-panel\"\n hx-swap=\"outerHTML\"\n >\n {csrfField()}\n <input type=\"hidden\" name=\"force\" value={latest ? \"true\" : \"false\"} />\n <Button variant=\"secondary\" size=\"sm\" type=\"submit\" icon=\"sparkles\">\n {latest ? \"Refresh health\" : \"Generate health\"}\n </Button>\n </form>\n ) : null}\n </VStack>\n </Card>\n </div>\n );\n}\n\n/** Renders the panel when AI health is available for the project, nothing otherwise. */\nexport function HealthSlot(props: {\n project: Project;\n data: HealthPanelData | null;\n}): HtmlEscapedString | Promise<HtmlEscapedString> | null {\n return props.data ? <HealthPanel project={props.project} data={props.data} /> : null;\n}\n"],"mappings":"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"}
|
package/dist/pages/invite.mjs
CHANGED
|
@@ -9,7 +9,7 @@ import { VStack } from "../ui/stacks.mjs";
|
|
|
9
9
|
import { csrfField } from "../ui/csrf-field.mjs";
|
|
10
10
|
import { DocumentLayout } from "../ui/document.mjs";
|
|
11
11
|
import { jsx, jsxs } from "hono/jsx/jsx-runtime";
|
|
12
|
-
|
|
12
|
+
// src/pages/invite.tsx
|
|
13
13
|
/** Set-password page for invite acceptance (no auth required). */
|
|
14
14
|
function renderInvitePage(state) {
|
|
15
15
|
return /* @__PURE__ */ jsx(DocumentLayout, {
|
|
@@ -85,7 +85,6 @@ function renderInviteInvalidPage(error) {
|
|
|
85
85
|
}) })
|
|
86
86
|
});
|
|
87
87
|
}
|
|
88
|
-
//#endregion
|
|
89
88
|
export { renderInviteInvalidPage, renderInvitePage };
|
|
90
89
|
|
|
91
90
|
//# sourceMappingURL=invite.mjs.map
|
|
@@ -10,7 +10,7 @@ import { Table } from "../ui/table.mjs";
|
|
|
10
10
|
import { DocumentLayout } from "../ui/document.mjs";
|
|
11
11
|
import { BuildModel, LabelModel, ProjectModel } from "@storyshelf/core/models";
|
|
12
12
|
import { Fragment, jsx, jsxs } from "hono/jsx/jsx-runtime";
|
|
13
|
-
|
|
13
|
+
// src/pages/label-detail.tsx
|
|
14
14
|
/** Resolve a label type's `link_template` against a build, or return null. */
|
|
15
15
|
function resolveLabelLink(template, project, build, value) {
|
|
16
16
|
if (!template) return null;
|
|
@@ -148,7 +148,6 @@ async function renderLabelsPage(slug) {
|
|
|
148
148
|
] }, labelType.id)) })] }) })]
|
|
149
149
|
});
|
|
150
150
|
}
|
|
151
|
-
//#endregion
|
|
152
151
|
export { renderLabelDetailPage, renderLabelsPage, resolveLabelLink };
|
|
153
152
|
|
|
154
153
|
//# sourceMappingURL=label-detail.mjs.map
|