canopycms 0.0.68-int.98 → 0.1.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/CHANGELOG.md +2131 -0
- package/README.md +47 -63
- package/dist/ai/generate.d.ts +5 -0
- package/dist/ai/generate.js +244 -115
- package/dist/ai/index.d.ts +1 -1
- package/dist/ai/json-to-markdown.d.ts +21 -1
- package/dist/ai/json-to-markdown.js +120 -57
- package/dist/ai/references.d.ts +72 -0
- package/dist/ai/references.js +223 -0
- package/dist/ai/resolve-branch.js +9 -2
- package/dist/ai/to-plain-text.d.ts +17 -8
- package/dist/ai/to-plain-text.js +18 -15
- package/dist/ai/types.d.ts +84 -3
- package/dist/api/admin-branch-health.d.ts +24 -1
- package/dist/api/admin-branch-health.js +52 -20
- package/dist/api/admin.d.ts +23 -5
- package/dist/api/admin.js +36 -1
- package/dist/api/assets.js +88 -78
- package/dist/api/branch-create-window.d.ts +12 -0
- package/dist/api/branch-create-window.js +13 -0
- package/dist/api/branch-merge.d.ts +1 -1
- package/dist/api/branch-merge.js +1 -1
- package/dist/api/branch-review.d.ts +3 -3
- package/dist/api/branch-review.js +16 -7
- package/dist/api/branch-status.d.ts +16 -6
- package/dist/api/branch-status.js +57 -12
- package/dist/api/branch-withdraw.d.ts +2 -2
- package/dist/api/branch-withdraw.js +8 -3
- package/dist/api/branch.d.ts +25 -12
- package/dist/api/branch.js +164 -103
- package/dist/api/client.d.ts +78 -58
- package/dist/api/client.js +150 -117
- package/dist/api/comments.d.ts +12 -3
- package/dist/api/comments.js +118 -9
- package/dist/api/content.d.ts +9 -9
- package/dist/api/content.js +131 -22
- package/dist/api/entries-constants.d.ts +14 -3
- package/dist/api/entries-constants.js +14 -3
- package/dist/api/entries.d.ts +40 -2
- package/dist/api/entries.js +102 -6
- package/dist/api/github-sync.d.ts +19 -0
- package/dist/api/github-sync.js +50 -3
- package/dist/api/groups.d.ts +4 -4
- package/dist/api/groups.js +2 -1
- package/dist/api/guards.js +23 -3
- package/dist/api/index.d.ts +3 -1
- package/dist/api/index.js +1 -0
- package/dist/api/permissions.d.ts +21 -2
- package/dist/api/permissions.js +50 -3
- package/dist/api/request-deadline.d.ts +59 -0
- package/dist/api/request-deadline.js +73 -0
- package/dist/api/resolve-references.js +8 -7
- package/dist/api/route-builder.d.ts +14 -0
- package/dist/api/route-builder.js +14 -1
- package/dist/api/schema.d.ts +11 -9
- package/dist/api/schema.js +2 -0
- package/dist/api/settings-helpers.d.ts +5 -1
- package/dist/api/settings-helpers.js +10 -2
- package/dist/api/types.d.ts +18 -0
- package/dist/api/users-constants.d.ts +6 -0
- package/dist/api/users-constants.js +6 -0
- package/dist/assets/asset-url.d.ts +28 -16
- package/dist/assets/asset-url.js +51 -25
- package/dist/assets/finalize.js +3 -0
- package/dist/assets/index.d.ts +1 -1
- package/dist/assets/materialize.d.ts +153 -0
- package/dist/assets/materialize.js +402 -0
- package/dist/assets/store-local.d.ts +19 -5
- package/dist/assets/store-local.js +69 -10
- package/dist/assets/store-s3.d.ts +38 -7
- package/dist/assets/store-s3.js +128 -22
- package/dist/assets/transform-directives.d.ts +52 -13
- package/dist/assets/transform-directives.js +87 -27
- package/dist/assets/transform.d.ts +3 -3
- package/dist/assets/transform.js +13 -8
- package/dist/assets/types.d.ts +31 -4
- package/dist/auth/cache.d.ts +1 -1
- package/dist/auth/cache.js +1 -1
- package/dist/auth/caching-auth-plugin.d.ts +2 -0
- package/dist/auth/caching-auth-plugin.js +12 -2
- package/dist/auth/file-based-auth-cache.d.ts +8 -0
- package/dist/auth/file-based-auth-cache.js +18 -8
- package/dist/auth/person-name.d.ts +5 -0
- package/dist/auth/person-name.js +8 -0
- package/dist/auth/plugin.d.ts +10 -1
- package/dist/auth/types.d.ts +2 -0
- package/dist/auth/user-metadata-lookup.d.ts +21 -0
- package/dist/auth/user-metadata-lookup.js +116 -0
- package/dist/authorization/branch.d.ts +1 -1
- package/dist/authorization/branch.js +12 -13
- package/dist/authorization/groups/schema.d.ts +4 -4
- package/dist/authorization/helpers.d.ts +8 -0
- package/dist/authorization/helpers.js +8 -0
- package/dist/authorization/index.d.ts +1 -1
- package/dist/authorization/index.js +1 -1
- package/dist/authorization/protected-branch.d.ts +5 -4
- package/dist/authorization/protected-branch.js +7 -5
- package/dist/branch-health.d.ts +35 -8
- package/dist/branch-health.js +54 -18
- package/dist/branch-metadata-error.d.ts +27 -0
- package/dist/branch-metadata-error.js +23 -0
- package/dist/branch-metadata-file.d.ts +205 -24
- package/dist/branch-metadata-file.js +126 -25
- package/dist/branch-metadata.d.ts +45 -6
- package/dist/branch-metadata.js +142 -50
- package/dist/branch-provisioning.d.ts +167 -0
- package/dist/branch-provisioning.js +501 -0
- package/dist/branch-registry.d.ts +8 -0
- package/dist/branch-registry.js +61 -9
- package/dist/branch-schema-cache.d.ts +29 -12
- package/dist/branch-schema-cache.js +64 -21
- package/dist/branch-sparse.d.ts +23 -0
- package/dist/branch-sparse.js +118 -0
- package/dist/branch-workspace.d.ts +42 -3
- package/dist/branch-workspace.js +239 -100
- package/dist/build/asset-refs.d.ts +94 -0
- package/dist/build/asset-refs.js +267 -0
- package/dist/build/generate-ai-content.js +5 -0
- package/dist/build/inline-flight.d.ts +37 -0
- package/dist/build/inline-flight.js +142 -0
- package/dist/cli/asset-refs.d.ts +52 -0
- package/dist/cli/asset-refs.js +194 -0
- package/dist/cli/cli.d.ts +15 -1
- package/dist/cli/cli.js +8543 -3193
- package/dist/cli/configured-asset-store.d.ts +12 -0
- package/dist/cli/configured-asset-store.js +46 -0
- package/dist/cli/generate-ai-content.js +4244 -1072
- package/dist/cli/init.js +217 -126
- package/dist/cli/project-detect.d.ts +2 -0
- package/dist/cli/project-detect.js +2 -0
- package/dist/cli/template-files/Dockerfile.cms.template +13 -5
- package/dist/cli/template-files/cms-stack.ts.template +16 -13
- package/dist/client.d.ts +1 -2
- package/dist/client.js +1 -2
- package/dist/comment-store.d.ts +25 -0
- package/dist/comment-store.js +43 -8
- package/dist/config/flatten.js +2 -0
- package/dist/config/helpers.js +2 -2
- package/dist/config/index.d.ts +1 -1
- package/dist/config/schemas/config.d.ts +12 -29
- package/dist/config/schemas/config.js +9 -12
- package/dist/config/schemas/field.d.ts +3 -0
- package/dist/config/schemas/field.js +58 -4
- package/dist/config/schemas/media.d.ts +4 -13
- package/dist/config/schemas/media.js +5 -8
- package/dist/config/schemas/url.d.ts +2 -20
- package/dist/config/schemas/url.js +2 -25
- package/dist/config/types.d.ts +116 -22
- package/dist/config/types.js +3 -9
- package/dist/config/validation.d.ts +25 -4
- package/dist/config/validation.js +116 -3
- package/dist/content-id-index.js +2 -2
- package/dist/content-index-generation.d.ts +1 -1
- package/dist/content-index-generation.js +1 -1
- package/dist/content-listing.d.ts +10 -1
- package/dist/content-listing.js +22 -5
- package/dist/content-reader.d.ts +9 -0
- package/dist/content-reader.js +16 -11
- package/dist/content-store.d.ts +75 -12
- package/dist/content-store.js +249 -86
- package/dist/content-tree.js +2 -2
- package/dist/context.js +5 -2
- package/dist/dangling-reference-log.d.ts +17 -0
- package/dist/dangling-reference-log.js +29 -0
- package/dist/dev-content-watcher.d.ts +1 -1
- package/dist/dev-content-watcher.js +1 -1
- package/dist/editor/BranchManager.d.ts +23 -7
- package/dist/editor/BranchManager.js +119 -54
- package/dist/editor/CanopyEditor.d.ts +1 -1
- package/dist/editor/CanopyEditor.js +1 -2
- package/dist/editor/CommentsPanel.d.ts +14 -0
- package/dist/editor/CommentsPanel.js +68 -43
- package/dist/editor/Editor.d.ts +11 -5
- package/dist/editor/Editor.js +891 -218
- package/dist/editor/EditorAuthGate.d.ts +7 -1
- package/dist/editor/EditorAuthGate.js +41 -9
- package/dist/editor/EntryNavigator.d.ts +22 -0
- package/dist/editor/EntryNavigator.js +353 -105
- package/dist/editor/FormHeader.d.ts +25 -0
- package/dist/editor/FormHeader.js +82 -0
- package/dist/editor/FormRenderer.d.ts +27 -3
- package/dist/editor/FormRenderer.js +171 -76
- package/dist/editor/PreviewFrame.d.ts +40 -0
- package/dist/editor/PreviewFrame.js +215 -0
- package/dist/editor/PreviewPane.d.ts +47 -0
- package/dist/editor/PreviewPane.js +147 -0
- package/dist/editor/PreviewToolbarTools.d.ts +22 -0
- package/dist/editor/PreviewToolbarTools.js +73 -0
- package/dist/editor/admin/SystemHealthPanel.d.ts +2 -1
- package/dist/editor/admin/SystemHealthPanel.js +163 -33
- package/dist/editor/admin/useSystemHealth.d.ts +18 -0
- package/dist/editor/admin/useSystemHealth.js +70 -1
- package/dist/editor/branch-status.d.ts +17 -0
- package/dist/editor/branch-status.js +29 -0
- package/dist/editor/canopy-path.d.ts +9 -6
- package/dist/editor/canopy-path.js +8 -11
- package/dist/editor/client-reference-resolver.d.ts +43 -16
- package/dist/editor/client-reference-resolver.js +155 -64
- package/dist/editor/comments/BranchComments.d.ts +1 -1
- package/dist/editor/comments/BranchComments.js +2 -2
- package/dist/editor/comments/CloseRefusedNotice.d.ts +4 -0
- package/dist/editor/comments/CloseRefusedNotice.js +7 -0
- package/dist/editor/comments/CommentThreadsPopover.d.ts +47 -0
- package/dist/editor/comments/CommentThreadsPopover.js +274 -0
- package/dist/editor/comments/CommentWriteAlert.d.ts +11 -0
- package/dist/editor/comments/CommentWriteAlert.js +13 -0
- package/dist/editor/comments/EntryComments.d.ts +10 -4
- package/dist/editor/comments/EntryComments.js +14 -17
- package/dist/editor/comments/FieldWrapper.d.ts +18 -4
- package/dist/editor/comments/FieldWrapper.js +62 -28
- package/dist/editor/comments/InlineCommentThread.d.ts +9 -2
- package/dist/editor/comments/InlineCommentThread.js +11 -47
- package/dist/editor/comments/ThreadCarousel.d.ts +1 -1
- package/dist/editor/comments/ThreadCarousel.js +18 -21
- package/dist/editor/comments/ThreadReplyControls.d.ts +28 -0
- package/dist/editor/comments/ThreadReplyControls.js +92 -0
- package/dist/editor/comments/field-comment-context.d.ts +14 -0
- package/dist/editor/comments/field-comment-context.js +14 -0
- package/dist/editor/comments/form-comments.d.ts +48 -0
- package/dist/editor/comments/form-comments.js +178 -0
- package/dist/editor/comments/item-anchor-copy.d.ts +6 -0
- package/dist/editor/comments/item-anchor-copy.js +6 -0
- package/dist/editor/comments/move-focus-if-unmoved.d.ts +6 -0
- package/dist/editor/comments/move-focus-if-unmoved.js +11 -0
- package/dist/editor/comments/popover-placement.d.ts +18 -0
- package/dist/editor/comments/popover-placement.js +58 -0
- package/dist/editor/comments/use-tracked-writes.d.ts +16 -0
- package/dist/editor/comments/use-tracked-writes.js +53 -0
- package/dist/editor/components/BranchAccessList.d.ts +15 -0
- package/dist/editor/components/BranchAccessList.js +52 -0
- package/dist/editor/components/BranchLockBanner.d.ts +22 -0
- package/dist/editor/components/BranchLockBanner.js +39 -0
- package/dist/editor/components/BranchPill.d.ts +32 -0
- package/dist/editor/components/BranchPill.js +33 -0
- package/dist/editor/components/BranchesDrawer.d.ts +11 -0
- package/dist/editor/components/BranchesDrawer.js +5 -0
- package/dist/editor/components/ConfirmDeleteModal.d.ts +6 -1
- package/dist/editor/components/ConfirmDeleteModal.js +3 -2
- package/dist/editor/components/DestructiveMenuItem.d.ts +7 -0
- package/dist/editor/components/DestructiveMenuItem.js +8 -0
- package/dist/editor/components/EditorCrashScreen.d.ts +17 -0
- package/dist/editor/components/EditorCrashScreen.js +22 -0
- package/dist/editor/components/EditorErrorBoundary.d.ts +38 -0
- package/dist/editor/components/EditorErrorBoundary.js +40 -0
- package/dist/editor/components/EditorHeader.d.ts +56 -106
- package/dist/editor/components/EditorHeader.js +130 -131
- package/dist/editor/components/EditorRail.d.ts +49 -0
- package/dist/editor/components/EditorRail.js +65 -0
- package/dist/editor/components/EntryBreadcrumb.d.ts +33 -0
- package/dist/editor/components/EntryBreadcrumb.js +56 -0
- package/dist/editor/components/EntryCreateModal.d.ts +12 -7
- package/dist/editor/components/EntryCreateModal.js +51 -57
- package/dist/editor/components/NoEditPermissionNotice.d.ts +6 -5
- package/dist/editor/components/NoEditPermissionNotice.js +5 -4
- package/dist/editor/components/QuickOpenPalette.d.ts +22 -0
- package/dist/editor/components/QuickOpenPalette.js +66 -0
- package/dist/editor/components/ReadOnlyDraftNotice.d.ts +11 -0
- package/dist/editor/components/ReadOnlyDraftNotice.js +12 -0
- package/dist/editor/components/ReasonedMenuItem.d.ts +15 -0
- package/dist/editor/components/ReasonedMenuItem.js +15 -0
- package/dist/editor/components/ReferencedByList.d.ts +11 -0
- package/dist/editor/components/ReferencedByList.js +28 -0
- package/dist/editor/components/RelativeTime.d.ts +10 -0
- package/dist/editor/components/RelativeTime.js +10 -0
- package/dist/editor/components/RenameEntryModal.d.ts +9 -1
- package/dist/editor/components/RenameEntryModal.js +19 -31
- package/dist/editor/components/RetryToastMessage.d.ts +9 -0
- package/dist/editor/components/RetryToastMessage.js +6 -0
- package/dist/editor/components/SaveAllResult.d.ts +25 -0
- package/dist/editor/components/SaveAllResult.js +86 -0
- package/dist/editor/components/SaveSplitButton.d.ts +30 -0
- package/dist/editor/components/SaveSplitButton.js +108 -0
- package/dist/editor/components/StagedChangesDrawer.d.ts +39 -0
- package/dist/editor/components/StagedChangesDrawer.js +92 -0
- package/dist/editor/components/UnavailableEntryNotice.d.ts +6 -0
- package/dist/editor/components/UnavailableEntryNotice.js +9 -0
- package/dist/editor/components/UnavailableTypeMessage.d.ts +6 -0
- package/dist/editor/components/UnavailableTypeMessage.js +9 -0
- package/dist/editor/components/UndoToastMessage.d.ts +18 -0
- package/dist/editor/components/UndoToastMessage.js +10 -0
- package/dist/editor/components/UserBadge.d.ts +2 -5
- package/dist/editor/components/UserBadge.js +62 -33
- package/dist/editor/components/ValidationSummary.d.ts +14 -0
- package/dist/editor/components/ValidationSummary.js +24 -0
- package/dist/editor/components/index.d.ts +2 -2
- package/dist/editor/components/index.js +2 -2
- package/dist/editor/context/ApiClientContext.d.ts +8 -2
- package/dist/editor/context/ApiClientContext.js +19 -8
- package/dist/editor/context/AssetContext.d.ts +13 -35
- package/dist/editor/context/AssetContext.js +12 -31
- package/dist/editor/context/EditorStateContext.d.ts +4 -4
- package/dist/editor/context/EditorStateContext.js +4 -4
- package/dist/editor/context/SWRProvider.d.ts +3 -3
- package/dist/editor/context/SiteMdxAllowContext.d.ts +4 -0
- package/dist/editor/context/SiteMdxAllowContext.js +7 -0
- package/dist/editor/context/index.d.ts +2 -1
- package/dist/editor/context/index.js +2 -1
- package/dist/editor/contrast.d.ts +19 -0
- package/dist/editor/contrast.js +41 -0
- package/dist/editor/copy.d.ts +18 -0
- package/dist/editor/copy.js +18 -0
- package/dist/editor/editor-config.js +16 -0
- package/dist/editor/editor-utils.d.ts +34 -8
- package/dist/editor/editor-utils.js +42 -20
- package/dist/editor/entry-search.d.ts +20 -0
- package/dist/editor/entry-search.js +34 -0
- package/dist/editor/field-breadcrumb.d.ts +12 -0
- package/dist/editor/field-breadcrumb.js +85 -0
- package/dist/editor/field-props.d.ts +86 -0
- package/dist/editor/field-props.js +27 -0
- package/dist/editor/field-target.d.ts +30 -0
- package/dist/editor/field-target.js +77 -0
- package/dist/editor/fields/BlockField.d.ts +12 -0
- package/dist/editor/fields/BlockField.js +89 -73
- package/dist/editor/fields/CodeField.d.ts +2 -0
- package/dist/editor/fields/CodeField.js +6 -2
- package/dist/editor/fields/DateTimeField.d.ts +4 -29
- package/dist/editor/fields/DateTimeField.js +79 -41
- package/dist/editor/fields/ExpandAllButton.d.ts +6 -0
- package/dist/editor/fields/ExpandAllButton.js +4 -0
- package/dist/editor/fields/FieldCrashFallback.d.ts +17 -0
- package/dist/editor/fields/FieldCrashFallback.js +30 -0
- package/dist/editor/fields/FieldLabel.d.ts +37 -0
- package/dist/editor/fields/FieldLabel.js +19 -0
- package/dist/editor/fields/ImageField.d.ts +4 -0
- package/dist/editor/fields/ImageField.js +29 -14
- package/dist/editor/fields/InlineGroupField.js +4 -3
- package/dist/editor/fields/ItemList.d.ts +42 -0
- package/dist/editor/fields/ItemList.js +130 -0
- package/dist/editor/fields/MarkdownField.d.ts +32 -0
- package/dist/editor/fields/MarkdownField.js +223 -46
- package/dist/editor/fields/MdxImageDialog.js +1 -1
- package/dist/editor/fields/NumberField.d.ts +2 -0
- package/dist/editor/fields/NumberField.js +8 -5
- package/dist/editor/fields/NumberListField.d.ts +2 -0
- package/dist/editor/fields/NumberListField.js +19 -16
- package/dist/editor/fields/ObjectField.d.ts +2 -7
- package/dist/editor/fields/ObjectField.js +15 -5
- package/dist/editor/fields/ObjectListField.d.ts +29 -0
- package/dist/editor/fields/ObjectListField.js +57 -0
- package/dist/editor/fields/ReferenceField.d.ts +2 -0
- package/dist/editor/fields/ReferenceField.js +34 -9
- package/dist/editor/fields/SelectField.d.ts +2 -0
- package/dist/editor/fields/SelectField.js +11 -2
- package/dist/editor/fields/StringListField.d.ts +4 -1
- package/dist/editor/fields/StringListField.js +22 -8
- package/dist/editor/fields/TextField.d.ts +2 -0
- package/dist/editor/fields/TextField.js +6 -2
- package/dist/editor/fields/ToggleField.d.ts +2 -0
- package/dist/editor/fields/ToggleField.js +8 -3
- package/dist/editor/fields/entry-link/InsertEntryLink.d.ts +1 -1
- package/dist/editor/fields/entry-link/InsertEntryLink.js +1 -1
- package/dist/editor/fields/form-expansion.d.ts +9 -0
- package/dist/editor/fields/form-expansion.js +2 -0
- package/dist/editor/fields/item-expansion.d.ts +15 -0
- package/dist/editor/fields/item-expansion.js +60 -0
- package/dist/editor/fields/list-item-keys.d.ts +8 -0
- package/dist/editor/fields/list-item-keys.js +50 -0
- package/dist/editor/fields/list-item-title.d.ts +20 -0
- package/dist/editor/fields/list-item-title.js +85 -0
- package/dist/editor/fields/markdown-export-options.d.ts +2 -0
- package/dist/editor/fields/markdown-export-options.js +8 -0
- package/dist/editor/fields/markdown-fidelity-visitors.d.ts +11 -0
- package/dist/editor/fields/markdown-fidelity-visitors.js +160 -0
- package/dist/editor/fields/mdx-jsx-support.d.ts +5 -1
- package/dist/editor/fields/mdx-jsx-support.js +157 -69
- package/dist/editor/fields/rich-text-failures.d.ts +5 -0
- package/dist/editor/fields/rich-text-failures.js +26 -0
- package/dist/editor/fields/toolbar-wrap.d.ts +7 -0
- package/dist/editor/fields/toolbar-wrap.js +35 -0
- package/dist/editor/form-outline.d.ts +13 -0
- package/dist/editor/form-outline.js +70 -0
- package/dist/editor/group-manager/GroupCard.d.ts +4 -1
- package/dist/editor/group-manager/GroupCard.js +12 -3
- package/dist/editor/group-manager/GroupForm.js +1 -1
- package/dist/editor/group-manager/InternalGroupsTab.d.ts +2 -1
- package/dist/editor/group-manager/InternalGroupsTab.js +4 -4
- package/dist/editor/group-manager/MemberList.d.ts +2 -1
- package/dist/editor/group-manager/MemberList.js +15 -4
- package/dist/editor/group-manager/hooks/useGroupState.d.ts +5 -0
- package/dist/editor/group-manager/hooks/useGroupState.js +30 -7
- package/dist/editor/group-manager/index.js +34 -5
- package/dist/editor/group-manager/types.d.ts +5 -2
- package/dist/editor/hooks/create-branch-request.d.ts +20 -0
- package/dist/editor/hooks/create-branch-request.js +54 -0
- package/dist/editor/hooks/save-all.d.ts +68 -0
- package/dist/editor/hooks/save-all.js +61 -0
- package/dist/editor/hooks/useBranchActions.d.ts +11 -1
- package/dist/editor/hooks/useBranchActions.js +42 -19
- package/dist/editor/hooks/useBranchManager.d.ts +21 -0
- package/dist/editor/hooks/useBranchManager.js +321 -125
- package/dist/editor/hooks/useBranchesData.d.ts +8 -4
- package/dist/editor/hooks/useBranchesData.js +9 -5
- package/dist/editor/hooks/useCommentSystem.d.ts +14 -2
- package/dist/editor/hooks/useCommentSystem.js +79 -55
- package/dist/editor/hooks/useCommentsData.d.ts +11 -7
- package/dist/editor/hooks/useCommentsData.js +20 -19
- package/dist/editor/hooks/useDraftManager.d.ts +77 -3
- package/dist/editor/hooks/useDraftManager.js +887 -94
- package/dist/editor/hooks/useEditorLayout.d.ts +53 -7
- package/dist/editor/hooks/useEditorLayout.js +98 -20
- package/dist/editor/hooks/useEntriesData.d.ts +2 -1
- package/dist/editor/hooks/useEntriesData.js +18 -4
- package/dist/editor/hooks/useEntryManager.d.ts +41 -2
- package/dist/editor/hooks/useEntryManager.js +227 -41
- package/dist/editor/hooks/useGroupManager.js +13 -16
- package/dist/editor/hooks/usePermissionManager.js +14 -6
- package/dist/editor/hooks/usePreviewMarks.d.ts +21 -0
- package/dist/editor/hooks/usePreviewMarks.js +33 -0
- package/dist/editor/hooks/useReferenceResolution.d.ts +10 -3
- package/dist/editor/hooks/useReferenceResolution.js +34 -177
- package/dist/editor/hooks/useSchemaManager.d.ts +25 -6
- package/dist/editor/hooks/useSchemaManager.js +97 -39
- package/dist/editor/hooks/useUserMetadata.d.ts +14 -4
- package/dist/editor/hooks/useUserMetadata.js +76 -47
- package/dist/editor/hooks/user-metadata-batcher.d.ts +8 -0
- package/dist/editor/hooks/user-metadata-batcher.js +33 -0
- package/dist/editor/media/AssetCard.d.ts +6 -7
- package/dist/editor/media/AssetCard.js +24 -31
- package/dist/editor/media/AssetListRow.d.ts +16 -0
- package/dist/editor/media/AssetListRow.js +41 -0
- package/dist/editor/media/CropStep.js +6 -1
- package/dist/editor/media/MediaLibrary.d.ts +4 -3
- package/dist/editor/media/MediaLibrary.js +11 -4
- package/dist/editor/media/MediaLibraryBody.d.ts +3 -3
- package/dist/editor/media/MediaLibraryBody.js +195 -80
- package/dist/editor/media/MediaLibrarySkeleton.d.ts +6 -0
- package/dist/editor/media/MediaLibrarySkeleton.js +14 -0
- package/dist/editor/media/crop-math.d.ts +3 -4
- package/dist/editor/media/crop-math.js +11 -22
- package/dist/editor/media/editor-image-src.d.ts +8 -0
- package/dist/editor/media/editor-image-src.js +10 -0
- package/dist/editor/media/format-bytes.d.ts +2 -0
- package/dist/editor/media/format-bytes.js +23 -0
- package/dist/editor/media/media-library-prefs.d.ts +23 -0
- package/dist/editor/media/media-library-prefs.js +49 -0
- package/dist/editor/media/media-library-view.d.ts +34 -0
- package/dist/editor/media/media-library-view.js +40 -0
- package/dist/editor/media/useAssetLibrary.d.ts +30 -0
- package/dist/editor/media/useAssetLibrary.js +126 -0
- package/dist/editor/media/useStableAssetOrder.d.ts +11 -0
- package/dist/editor/media/useStableAssetOrder.js +37 -0
- package/dist/editor/media/useThumbnailFailure.d.ts +6 -0
- package/dist/editor/media/useThumbnailFailure.js +16 -0
- package/dist/editor/permission-manager/GroupSelector.js +2 -2
- package/dist/editor/permission-manager/PermissionEditor.d.ts +1 -1
- package/dist/editor/permission-manager/PermissionEditor.js +15 -4
- package/dist/editor/permission-manager/PermissionLevelBadge.js +1 -1
- package/dist/editor/permission-manager/PermissionTree.d.ts +1 -1
- package/dist/editor/permission-manager/UserSelector.d.ts +1 -1
- package/dist/editor/permission-manager/UserSelector.js +3 -3
- package/dist/editor/permission-manager/index.js +12 -3
- package/dist/editor/permission-manager/types.d.ts +5 -2
- package/dist/editor/platform.d.ts +3 -0
- package/dist/editor/platform.js +9 -0
- package/dist/editor/preview-asset-base.d.ts +9 -0
- package/dist/editor/preview-asset-base.js +17 -0
- package/dist/editor/preview-bridge.d.ts +51 -27
- package/dist/editor/preview-bridge.js +103 -144
- package/dist/editor/preview-marks.d.ts +15 -0
- package/dist/editor/preview-marks.js +74 -0
- package/dist/editor/preview-path.d.ts +12 -0
- package/dist/editor/preview-path.js +45 -1
- package/dist/editor/preview-scale.d.ts +27 -0
- package/dist/editor/preview-scale.js +34 -0
- package/dist/editor/raw-asset-base.d.ts +5 -0
- package/dist/editor/raw-asset-base.js +14 -0
- package/dist/editor/relative-time.d.ts +4 -4
- package/dist/editor/relative-time.js +16 -8
- package/dist/editor/schema-editor/CollectionEditor.d.ts +20 -3
- package/dist/editor/schema-editor/CollectionEditor.js +361 -62
- package/dist/editor/schema-editor/EntryTypeEditor.d.ts +5 -1
- package/dist/editor/schema-editor/EntryTypeEditor.js +11 -6
- package/dist/editor/schema-editor/collection-draft.d.ts +22 -0
- package/dist/editor/schema-editor/collection-draft.js +40 -0
- package/dist/editor/schema-editor/index.d.ts +1 -1
- package/dist/editor/schema-label.d.ts +10 -0
- package/dist/editor/schema-label.js +17 -0
- package/dist/editor/settings-conflict.d.ts +5 -0
- package/dist/editor/settings-conflict.js +8 -0
- package/dist/editor/shell/ActivityRail.d.ts +37 -0
- package/dist/editor/shell/ActivityRail.js +91 -0
- package/dist/editor/shell/EditorShell.d.ts +17 -0
- package/dist/editor/shell/EditorShell.js +31 -0
- package/dist/editor/shell/PaneLayout.d.ts +28 -0
- package/dist/editor/shell/PaneLayout.js +116 -0
- package/dist/editor/shell/SidePanelHost.d.ts +35 -0
- package/dist/editor/shell/SidePanelHost.js +184 -0
- package/dist/editor/shell/TopBar.d.ts +12 -0
- package/dist/editor/shell/TopBar.js +14 -0
- package/dist/editor/shell/host-chrome.d.ts +16 -0
- package/dist/editor/shell/host-chrome.js +69 -0
- package/dist/editor/slug.d.ts +16 -0
- package/dist/editor/slug.js +43 -0
- package/dist/editor/theme.d.ts +2 -1
- package/dist/editor/theme.js +314 -13
- package/dist/editor/unanswered.d.ts +17 -0
- package/dist/editor/unanswered.js +26 -0
- package/dist/editor/unavailable-entry-type.d.ts +20 -0
- package/dist/editor/unavailable-entry-type.js +28 -0
- package/dist/editor/undo-removal.d.ts +57 -0
- package/dist/editor/undo-removal.js +163 -0
- package/dist/editor/user-display.d.ts +4 -0
- package/dist/editor/user-display.js +5 -0
- package/dist/editor/utils/confirm-modal.d.ts +8 -0
- package/dist/editor/utils/confirm-modal.js +12 -0
- package/dist/editor/utils/editor-errors.d.ts +19 -0
- package/dist/editor/utils/editor-errors.js +32 -0
- package/dist/editor/utils/focus-return.d.ts +56 -0
- package/dist/editor/utils/focus-return.js +106 -0
- package/dist/entry-link-resolver.d.ts +11 -1
- package/dist/entry-link-resolver.js +65 -5
- package/dist/entry-schema-registry.d.ts +3 -3
- package/dist/entry-schema-registry.js +7 -4
- package/dist/entry-schema.d.ts +49 -3
- package/dist/entry-schema.js +18 -0
- package/dist/git-manager.d.ts +172 -10
- package/dist/git-manager.js +449 -107
- package/dist/github-service.d.ts +39 -2
- package/dist/github-service.js +101 -7
- package/dist/http/handler.js +74 -9
- package/dist/http/types.d.ts +2 -0
- package/dist/http/worker-not-ready.d.ts +9 -4
- package/dist/http/worker-not-ready.js +59 -6
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/item-keys.d.ts +46 -0
- package/dist/item-keys.js +235 -0
- package/dist/operating-mode/editor-mode-check.d.ts +15 -0
- package/dist/operating-mode/editor-mode-check.js +24 -0
- package/dist/operating-mode/mode-env.d.ts +13 -14
- package/dist/operating-mode/mode-env.js +13 -14
- package/dist/operating-mode/types.d.ts +2 -2
- package/dist/paths/branch.d.ts +0 -1
- package/dist/paths/branch.js +1 -8
- package/dist/paths/index.d.ts +1 -1
- package/dist/paths/index.js +1 -1
- package/dist/paths/validation.js +0 -2
- package/dist/preview.d.ts +8 -0
- package/dist/preview.js +7 -0
- package/dist/reference-resolver.js +1 -0
- package/dist/resolve-canopy-user.d.ts +1 -1
- package/dist/resolve-canopy-user.js +1 -1
- package/dist/schema/meta-loader.d.ts +18 -8
- package/dist/schema/meta-loader.js +42 -9
- package/dist/schema/registry-fingerprint.d.ts +14 -0
- package/dist/schema/registry-fingerprint.js +63 -0
- package/dist/schema/resolver.d.ts +7 -4
- package/dist/schema/resolver.js +7 -6
- package/dist/schema/schema-store.d.ts +7 -10
- package/dist/schema/schema-store.js +13 -11
- package/dist/schema/schema-unavailable-error.d.ts +11 -0
- package/dist/schema/schema-unavailable-error.js +15 -0
- package/dist/schema/types.d.ts +22 -0
- package/dist/schema-registry-record.d.ts +35 -0
- package/dist/schema-registry-record.js +134 -0
- package/dist/server.d.ts +79 -9
- package/dist/server.js +62 -8
- package/dist/services.d.ts +33 -5
- package/dist/services.js +155 -37
- package/dist/settings-workspace.d.ts +23 -3
- package/dist/settings-workspace.js +159 -45
- package/dist/stamp-on-publish.d.ts +36 -0
- package/dist/stamp-on-publish.js +71 -0
- package/dist/static/index.d.ts +58 -5
- package/dist/static/index.js +133 -3
- package/dist/static/seo.d.ts +5 -5
- package/dist/static/seo.js +5 -5
- package/dist/submission-attribution.d.ts +8 -0
- package/dist/submission-attribution.js +22 -0
- package/dist/system-users.d.ts +12 -0
- package/dist/system-users.js +12 -0
- package/dist/task-queue/cms-task-queue.d.ts +1 -1
- package/dist/task-queue/cms-task-queue.js +1 -1
- package/dist/task-queue/index.d.ts +1 -1
- package/dist/task-queue/index.js +1 -1
- package/dist/task-queue/task-queue.d.ts +6 -0
- package/dist/task-queue/task-queue.js +29 -0
- package/dist/task-queue/worker-status.d.ts +35 -9
- package/dist/task-queue/worker-status.js +84 -17
- package/dist/types.d.ts +90 -9
- package/dist/url-collision.js +3 -3
- package/dist/url-exclusivity-fixtures.d.ts +8 -1
- package/dist/url-exclusivity-fixtures.js +11 -1
- package/dist/user.js +2 -1
- package/dist/utils/atomic-write.js +2 -1
- package/dist/utils/base-branch.d.ts +13 -0
- package/dist/utils/base-branch.js +23 -0
- package/dist/utils/content-serialize.d.ts +15 -7
- package/dist/utils/content-serialize.js +353 -51
- package/dist/utils/content-write-lock.d.ts +6 -1
- package/dist/utils/content-write-lock.js +21 -6
- package/dist/utils/debug.d.ts +1 -1
- package/dist/utils/debug.js +5 -3
- package/dist/utils/error.js +13 -2
- package/dist/utils/escape-text.d.ts +7 -0
- package/dist/utils/escape-text.js +27 -0
- package/dist/utils/fs.d.ts +12 -4
- package/dist/utils/fs.js +18 -5
- package/dist/utils/git.d.ts +21 -1
- package/dist/utils/git.js +63 -8
- package/dist/utils/json-source-splice.d.ts +14 -0
- package/dist/utils/json-source-splice.js +120 -0
- package/dist/utils/markdown-body-splice.d.ts +39 -0
- package/dist/utils/markdown-body-splice.js +421 -0
- package/dist/utils/occ-json-write.js +18 -12
- package/dist/utils/provision-log.d.ts +25 -0
- package/dist/utils/provision-log.js +49 -0
- package/dist/utils/provisioning-lock.d.ts +32 -1
- package/dist/utils/provisioning-lock.js +78 -32
- package/dist/utils/sanitize-href.d.ts +9 -23
- package/dist/utils/sanitize-href.js +14 -28
- package/dist/utils/title-field.d.ts +10 -0
- package/dist/utils/title-field.js +47 -0
- package/dist/utils/yaml-source-splice.d.ts +58 -0
- package/dist/utils/yaml-source-splice.js +515 -0
- package/dist/validation/block-structural-keys.d.ts +1 -1
- package/dist/validation/block-structural-keys.js +1 -1
- package/dist/validation/deletion-checker.d.ts +21 -29
- package/dist/validation/deletion-checker.js +41 -117
- package/dist/validation/entry-validator.d.ts +6 -10
- package/dist/validation/entry-validator.js +19 -26
- package/dist/validation/field-traversal.d.ts +22 -0
- package/dist/validation/field-traversal.js +39 -8
- package/dist/validation/markdown-safety.d.ts +68 -0
- package/dist/validation/markdown-safety.js +596 -0
- package/dist/validation/mdx-allowlist.d.ts +55 -0
- package/dist/validation/mdx-allowlist.js +183 -0
- package/dist/validation/reference-validator.d.ts +2 -0
- package/dist/validation/reference-validator.js +3 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker/branch-policy.d.ts +124 -0
- package/dist/worker/branch-policy.js +243 -0
- package/dist/worker/bundle-header.d.ts +117 -0
- package/dist/worker/bundle-header.js +253 -0
- package/dist/worker/cms-worker.d.ts +236 -142
- package/dist/worker/cms-worker.js +772 -285
- package/dist/worker/gateway-egress.d.ts +43 -0
- package/dist/worker/gateway-egress.js +209 -0
- package/dist/worker/gateway-instance.d.ts +42 -0
- package/dist/worker/gateway-instance.js +129 -0
- package/dist/worker/git-process.d.ts +81 -0
- package/dist/worker/git-process.js +234 -0
- package/dist/worker/git-sync.d.ts +34 -17
- package/dist/worker/git-sync.js +394 -131
- package/dist/worker/github-auth.d.ts +9 -9
- package/dist/worker/github-auth.js +13 -15
- package/dist/worker/github-gateway-http.d.ts +62 -0
- package/dist/worker/github-gateway-http.js +977 -0
- package/dist/worker/github-gateway-wire.d.ts +590 -0
- package/dist/worker/github-gateway-wire.js +488 -0
- package/dist/worker/github-gateway.d.ts +238 -0
- package/dist/worker/github-gateway.js +726 -0
- package/dist/worker/github-mirror-upkeep.d.ts +75 -0
- package/dist/worker/github-mirror-upkeep.js +266 -0
- package/dist/worker/github-mirror.d.ts +364 -0
- package/dist/worker/github-mirror.js +1030 -0
- package/dist/worker/github-transfer.d.ts +56 -0
- package/dist/worker/github-transfer.js +444 -0
- package/dist/worker/history-rewrite.d.ts +4 -5
- package/dist/worker/history-rewrite.js +12 -17
- package/dist/worker/provisioned-workspace.d.ts +5 -5
- package/dist/worker/provisioned-workspace.js +5 -5
- package/dist/worker/rebase.d.ts +1 -1
- package/dist/worker/rebase.js +58 -54
- package/dist/worker/remote-git-maintenance.d.ts +3 -0
- package/dist/worker/remote-git-maintenance.js +12 -0
- package/dist/worker/schema-gate.d.ts +49 -0
- package/dist/worker/schema-gate.js +226 -0
- package/dist/worker/self-checks.d.ts +107 -0
- package/dist/worker/self-checks.js +358 -0
- package/dist/worker/shared-repo-git.d.ts +96 -0
- package/dist/worker/shared-repo-git.js +459 -0
- package/dist/worker/sparse-cone.d.ts +20 -0
- package/dist/worker/sparse-cone.js +136 -0
- package/dist/worker/stale-mirror.d.ts +82 -0
- package/dist/worker/stale-mirror.js +130 -0
- package/dist/worker/task-runner.d.ts +5 -3
- package/dist/worker/task-runner.js +287 -186
- package/dist/worker/worker-context.d.ts +36 -37
- package/package.json +35 -7
- package/dist/editor/EditorPanes.d.ts +0 -10
- package/dist/editor/EditorPanes.js +0 -98
- package/dist/editor/components/EditorFooter.d.ts +0 -11
- package/dist/editor/components/EditorFooter.js +0 -9
- package/dist/editor/components/EditorSidebar.d.ts +0 -67
- package/dist/editor/components/EditorSidebar.js +0 -22
|
@@ -9,10 +9,15 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
9
9
|
* only for admins (see isAdmin(userContext?.groups)); this component does not
|
|
10
10
|
* re-check that itself, same as GroupManager and PermissionManager.
|
|
11
11
|
*/
|
|
12
|
-
import { Alert, Badge, Button, Code, Group, Loader, Modal, Paper, SegmentedControl, SimpleGrid, Spoiler, Stack, Table, Tabs, Text, ThemeIcon, Tooltip, } from '@mantine/core';
|
|
12
|
+
import { Alert, Badge, Box, Button, Code, Group, Loader, Modal, Paper, SegmentedControl, SimpleGrid, Spoiler, Stack, Table, Tabs, Text, ThemeIcon, Tooltip, VisuallyHidden, } from '@mantine/core';
|
|
13
|
+
import { useId } from 'react';
|
|
13
14
|
import { IconAlertCircle, IconAlertTriangle } from '@tabler/icons-react';
|
|
14
|
-
import {
|
|
15
|
+
import { openConfirm } from '../utils/confirm-modal.js';
|
|
16
|
+
import { focusReturnProps } from '../utils/focus-return.js';
|
|
17
|
+
import { formatRelativeTime } from '../relative-time.js';
|
|
15
18
|
import { useSystemHealth, } from './useSystemHealth.js';
|
|
19
|
+
import { escapeForLog } from '../../utils/escape-text.js';
|
|
20
|
+
import { branchStatusPresentation } from '../branch-status.js';
|
|
16
21
|
// ============================================================================
|
|
17
22
|
// Small pure helpers
|
|
18
23
|
// ============================================================================
|
|
@@ -49,21 +54,33 @@ function formatAgeMs(ms) {
|
|
|
49
54
|
*/
|
|
50
55
|
function purgeGateFor(entry) {
|
|
51
56
|
if (entry.isBaseBranch) {
|
|
52
|
-
return { disabled: true, tooltip: 'The base branch can never be purged' };
|
|
57
|
+
return { disabled: true, tooltip: 'The base branch can never be purged.' };
|
|
53
58
|
}
|
|
54
59
|
const lockFresh = !!entry.provisioningLock && entry.provisioningLock.ageMs < PROVISIONING_LOCK_FRESH_MS;
|
|
55
60
|
if (entry.kind === 'orphan') {
|
|
56
61
|
const tooYoung = (entry.ageMs ?? 0) < ORPHAN_YOUTH_THRESHOLD_MS;
|
|
57
62
|
const disabled = lockFresh || tooYoung;
|
|
58
|
-
return {
|
|
63
|
+
return {
|
|
64
|
+
disabled,
|
|
65
|
+
tooltip: disabled
|
|
66
|
+
? 'This may be a clone in progress. Try again in a few minutes.'
|
|
67
|
+
: undefined,
|
|
68
|
+
};
|
|
59
69
|
}
|
|
60
70
|
return {
|
|
61
71
|
disabled: lockFresh,
|
|
62
72
|
tooltip: lockFresh
|
|
63
|
-
? 'Provisioning, or the worker syncing this branch, may be in progress'
|
|
73
|
+
? 'Provisioning, or the worker syncing this branch, may be in progress. Try again in a few minutes.'
|
|
64
74
|
: undefined,
|
|
65
75
|
};
|
|
66
76
|
}
|
|
77
|
+
/** Purge stays focusable while gated (`aria-disabled`), so its reason opens on focus and touch. */
|
|
78
|
+
function PurgeButton({ entry, gate, onPurge, }) {
|
|
79
|
+
return (_jsx(Tooltip, { label: gate.tooltip, disabled: !gate.disabled, children: _jsx(Button, { variant: "light", color: "red", "data-disabled": gate.disabled ? true : undefined, "aria-disabled": gate.disabled ? true : undefined, "data-testid": `purge-dir-${entry.dirName}`, onClick: () => {
|
|
80
|
+
if (!gate.disabled)
|
|
81
|
+
onPurge(entry.dirName);
|
|
82
|
+
}, children: "Purge" }) }));
|
|
83
|
+
}
|
|
67
84
|
function workerLivenessBadge(worker, mode) {
|
|
68
85
|
if (mode === 'dev') {
|
|
69
86
|
return { color: 'gray', label: `Worker: ${worker.state}` };
|
|
@@ -86,6 +103,25 @@ const BASE_REFRESH_LABELS = {
|
|
|
86
103
|
'skipped-not-provisioned': 'not yet provisioned',
|
|
87
104
|
failed: 'refresh failed',
|
|
88
105
|
};
|
|
106
|
+
/**
|
|
107
|
+
* The worker holding the base branch for an editor deploy, or the cycle it stopped waiting.
|
|
108
|
+
* The rule and its bound live in worker/schema-gate.ts.
|
|
109
|
+
*/
|
|
110
|
+
function BaseHoldAlert({ hold }) {
|
|
111
|
+
const names = hold.missingSchemas.map((name, i) => (_jsxs("span", { children: [i > 0 && ', ', _jsx(Code, { children: name })] }, name)));
|
|
112
|
+
const editor = hold.editorBuild.sourceRevision ? (_jsxs(_Fragment, { children: ["the running editor (built from ", _jsx(Code, { children: hold.editorBuild.sourceRevision.slice(0, 12) }), ")"] })) : ('the running editor');
|
|
113
|
+
return (_jsxs(Alert, { color: hold.expired ? 'orange' : 'blue', icon: _jsx(IconAlertTriangle, { size: 16 }), title: hold.expired ? 'Stopped waiting for the editor deploy' : 'Waiting for editor deploy', "data-testid": "base-hold-alert", children: [_jsxs(Text, { size: "sm", children: ["Newly merged content names ", names, ", which ", editor, " does not define.", ' ', hold.expired
|
|
114
|
+
? 'The worker updated the base branch anyway; content types using them are unavailable until an editor image defining them is deployed.'
|
|
115
|
+
: 'The worker keeps the base branch at its current version until an editor image defining them handles a request.'] }), _jsxs(Text, { size: "xs", c: "dimmed", mt: 4, children: ["Held since ", hold.since, " \u00B7 ", hold.files.join(', '), hold.fileCount > hold.files.length && ` and ${hold.fileCount - hold.files.length} more`] })] }));
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Branches the worker left diverged from GitHub because this deployment's copy holds editor work
|
|
119
|
+
* (worker/git-sync.ts's `reconcileTrackedBranches`). A copy holding none is moved to GitHub's tip
|
|
120
|
+
* and never listed. Names come from GitHub and are attacker-chosen, so each is shown escaped.
|
|
121
|
+
*/
|
|
122
|
+
function DivergedBranchesAlert({ names }) {
|
|
123
|
+
return (_jsx(Alert, { color: "orange", icon: _jsx(IconAlertTriangle, { size: 16 }), title: `${plural(names.length, 'branch')} diverged from GitHub`, "data-testid": "diverged-branches-alert", children: _jsxs(Stack, { gap: 4, children: [_jsx(Text, { size: "sm", children: "This deployment's copy of each branch below holds editor work that conflicts with the branch on GitHub, usually because the GitHub branch was rebased or force-pushed, or because another CanopyCMS deployment sharing this repository uses the same branch name. Neither copy has been changed, and publishing the branch fails until they agree." }), _jsx(Text, { size: "sm", children: "A developer decides which version to keep. To keep GitHub's, first push it to a new branch name on GitHub, because \"Delete branch\" in the editor also deletes a published branch on GitHub; then delete this branch in the editor (withdraw it from review first if it is in review). The editor's changes cannot be copied to another branch: to keep them, leave this branch as it is, or re-enter them by hand in a new branch. For the base branch or a settings branch, ask a developer to check what changed on GitHub." }), _jsx(Group, { gap: 4, children: names.map((name) => (_jsx(Code, { children: escapeForLog(name) }, name))) })] }) }));
|
|
124
|
+
}
|
|
89
125
|
/**
|
|
90
126
|
* Why the base branch needs an operator, or null when its last refresh needs
|
|
91
127
|
* nothing. Shared by the overview and the base row's warning tooltip.
|
|
@@ -105,13 +141,6 @@ function baseRefreshWarning(report) {
|
|
|
105
141
|
}
|
|
106
142
|
return lines.length > 0 ? lines.join('\n') : null;
|
|
107
143
|
}
|
|
108
|
-
// Mirrors BranchManager.tsx's statusColorMap -- kept local (not exported
|
|
109
|
-
// there) rather than shared, same tiny lookup either way.
|
|
110
|
-
const branchStatusColorMap = {
|
|
111
|
-
editing: 'brand',
|
|
112
|
-
submitted: 'green',
|
|
113
|
-
approved: 'teal',
|
|
114
|
-
};
|
|
115
144
|
const TASK_STATUS_OPTIONS = [
|
|
116
145
|
{ label: 'Pending', value: 'pending' },
|
|
117
146
|
{ label: 'Processing', value: 'processing' },
|
|
@@ -134,10 +163,63 @@ function deleteConfirmText(status) {
|
|
|
134
163
|
}
|
|
135
164
|
const MARK_MERGED_CONFIRM_TEXT = 'In production the server cannot verify the PR actually merged (no GitHub access from the API) — confirm the PR is merged on GitHub first.';
|
|
136
165
|
const REPAIR_CONFIRM_TEXT = "Recreates metadata with defaults: status becomes 'editing', you become the creator, branch ACLs are reset. The corrupt file is archived alongside for forensics.";
|
|
166
|
+
const DUPLICATE_REPAIR_CONFIRM_TEXT = 'These files share a content ID, so the editor uses only the one marked Keep. Archiving renames each other file to a hidden name in the same folder. Nothing is deleted.';
|
|
167
|
+
const DUPLICATE_REPAIR_EDITOR_NOTE = 'This changes files on the branch, and adds you as one of its editors.';
|
|
168
|
+
/**
|
|
169
|
+
* Why a healthy row's duplicate-ID state is unknown. `null` is a row the last
|
|
170
|
+
* scan did not include.
|
|
171
|
+
*/
|
|
172
|
+
function duplicateUncheckedReason(scan) {
|
|
173
|
+
if (scan?.state === 'unknown' && scan.reason === 'failed') {
|
|
174
|
+
return 'The duplicate ID check failed on this branch.';
|
|
175
|
+
}
|
|
176
|
+
if (scan?.state === 'unknown') {
|
|
177
|
+
return 'The duplicate ID check ran out of time before reaching this branch.';
|
|
178
|
+
}
|
|
179
|
+
return 'This branch was added after the last duplicate ID check.';
|
|
180
|
+
}
|
|
181
|
+
function plural(n, word) {
|
|
182
|
+
return `${n} ${word}${n === 1 ? '' : word.endsWith('h') ? 'es' : 's'}`;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* One line for the whole Branches tab, so a scan that did not finish never
|
|
186
|
+
* reads as a clean one: rows without a badge are clean only when this says so.
|
|
187
|
+
*/
|
|
188
|
+
function duplicateScanSummary(health, healthyDirNames) {
|
|
189
|
+
const { duplicateIdScan: scan, duplicateIdScanLoading, duplicateIdScanError } = health;
|
|
190
|
+
if (duplicateIdScanLoading && !scan) {
|
|
191
|
+
return { text: 'Checking for duplicate content IDs…', color: 'dimmed' };
|
|
192
|
+
}
|
|
193
|
+
if (duplicateIdScanError) {
|
|
194
|
+
return {
|
|
195
|
+
text: `Could not check for duplicate content IDs: ${duplicateIdScanError}`,
|
|
196
|
+
color: 'orange',
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
if (!scan)
|
|
200
|
+
return { text: 'Duplicate content IDs not checked yet.', color: 'orange' };
|
|
201
|
+
let found = 0;
|
|
202
|
+
let unchecked = 0;
|
|
203
|
+
for (const dirName of healthyDirNames) {
|
|
204
|
+
const state = scan.byDir[dirName]?.state;
|
|
205
|
+
if (state === 'found')
|
|
206
|
+
found++;
|
|
207
|
+
else if (state !== 'none')
|
|
208
|
+
unchecked++;
|
|
209
|
+
}
|
|
210
|
+
const parts = [];
|
|
211
|
+
if (found > 0)
|
|
212
|
+
parts.push(`Duplicate content IDs found on ${plural(found, 'branch')}.`);
|
|
213
|
+
if (unchecked > 0)
|
|
214
|
+
parts.push(`Could not check ${plural(unchecked, 'branch')}.`);
|
|
215
|
+
if (parts.length === 0)
|
|
216
|
+
return { text: 'No duplicate content IDs found.', color: 'dimmed' };
|
|
217
|
+
return { text: parts.join(' '), color: 'orange' };
|
|
218
|
+
}
|
|
137
219
|
const PURGE_CONFIRM_TEXT = 'The directory is moved to a hidden trash name and kept for 30 days, then deleted. Any git work inside was never pushed and will be lost when the trash is swept.';
|
|
138
|
-
export function SystemHealthPanel({ opened, onClose }) {
|
|
220
|
+
export function SystemHealthPanel({ opened, onClose, returnFocusTo }) {
|
|
139
221
|
const health = useSystemHealth({ isOpen: opened });
|
|
140
|
-
return (_jsx(Modal, { opened: opened, onClose: onClose, title: "System health", size: "xl", children: _jsxs(Tabs, { defaultValue: "overview", children: [_jsxs(Tabs.List, { children: [_jsx(Tabs.Tab, { value: "overview", children: "Overview" }), _jsx(Tabs.Tab, { value: "tasks", children: "Tasks" }), _jsx(Tabs.Tab, { value: "branches", children: "Branches" })] }), _jsx(Tabs.Panel, { value: "overview", pt: "md", children: _jsx(OverviewTab, { health: health }) }), _jsx(Tabs.Panel, { value: "tasks", pt: "md", children: _jsx(TasksTab, { health: health }) }), _jsx(Tabs.Panel, { value: "branches", pt: "md", children: _jsx(BranchesTab, { health: health }) })] }) }));
|
|
222
|
+
return (_jsx(Modal, { opened: opened, onClose: onClose, title: "System health", size: "xl", ...focusReturnProps(returnFocusTo), children: _jsxs(Tabs, { defaultValue: "overview", children: [_jsxs(Tabs.List, { children: [_jsx(Tabs.Tab, { value: "overview", children: "Overview" }), _jsx(Tabs.Tab, { value: "tasks", children: "Tasks" }), _jsx(Tabs.Tab, { value: "branches", children: "Branches" })] }), _jsx(Tabs.Panel, { value: "overview", pt: "md", children: _jsx(OverviewTab, { health: health }) }), _jsx(Tabs.Panel, { value: "tasks", pt: "md", children: _jsx(TasksTab, { health: health }) }), _jsx(Tabs.Panel, { value: "branches", pt: "md", children: _jsx(BranchesTab, { health: health }) })] }) }));
|
|
141
223
|
}
|
|
142
224
|
function OverviewTab({ health }) {
|
|
143
225
|
const { status, statusLoading, isRecentFatalError, error, refresh } = health;
|
|
@@ -145,7 +227,7 @@ function OverviewTab({ health }) {
|
|
|
145
227
|
return (_jsxs(Group, { justify: "center", py: "xl", children: [_jsx(Loader, { size: "md" }), _jsx(Text, { size: "sm", c: "dimmed", children: "Loading status..." })] }));
|
|
146
228
|
}
|
|
147
229
|
if (!status) {
|
|
148
|
-
return (_jsxs(Stack, { align: "center", py: "xl", gap: "sm", children: [_jsx(Text, { size: "sm", c: "dimmed", children: "No status available." }), _jsx(Button, {
|
|
230
|
+
return (_jsxs(Stack, { align: "center", py: "xl", gap: "sm", children: [_jsx(Text, { size: "sm", c: "dimmed", children: "No status available." }), _jsx(Button, { variant: "light", onClick: () => refresh(), children: "Refresh" })] }));
|
|
149
231
|
}
|
|
150
232
|
const liveness = workerLivenessBadge(status.worker, status.mode);
|
|
151
233
|
const lastFatalError = status.workerStatus?.lastFatalError;
|
|
@@ -155,12 +237,22 @@ function OverviewTab({ health }) {
|
|
|
155
237
|
// Absent for a worker that predates the field, which is not evidence of skew;
|
|
156
238
|
// nor is a stale or absent worker's leftover status file, which names no running build.
|
|
157
239
|
const workerVersion = status.workerStatus?.workerVersion || undefined;
|
|
240
|
+
const lastShutdown = status.workerStatus?.lastShutdown;
|
|
158
241
|
const versionSkew = status.worker.state === 'alive' &&
|
|
159
242
|
workerVersion !== undefined &&
|
|
160
243
|
workerVersion !== build.canopycmsVersion;
|
|
161
|
-
return (_jsxs(Stack, { gap: "md", children: [error && (_jsx(Alert, { color: "red", icon: _jsx(IconAlertCircle, { size: 16 }), variant: "light", children: error })), _jsxs(Group, { gap: "sm", children: [_jsx(Badge, { color: liveness.color, variant: "light", size: "lg", children: liveness.label }), status.mode === 'dev' && (_jsx(Text, { size: "xs", c: "dimmed", children: "No worker runs in dev mode" }))] }), isRecentFatalError && lastFatalError && (_jsxs(Alert, { color: "red", icon: _jsx(IconAlertCircle, { size: 16 }), title:
|
|
244
|
+
return (_jsxs(Stack, { gap: "md", children: [error && (_jsx(Alert, { color: "red", icon: _jsx(IconAlertCircle, { size: 16 }), variant: "light", children: error })), _jsxs(Group, { gap: "sm", children: [_jsx(Badge, { color: liveness.color, variant: "light", size: "lg", children: liveness.label }), status.mode === 'dev' && (_jsx(Text, { size: "xs", c: "dimmed", children: "No worker runs in dev mode" }))] }), isRecentFatalError && lastFatalError && (_jsxs(Alert, { color: "red", icon: _jsx(IconAlertCircle, { size: 16 }), title: lastFatalError.phase === 'startup'
|
|
245
|
+
? 'The worker failed to start'
|
|
246
|
+
: 'The worker stopped while running', children: [_jsx(Text, { size: "sm", children: lastFatalError.message }), _jsxs(Text, { size: "xs", c: "dimmed", mt: 4, children: ["at ", lastFatalError.at] })] })), status.settingsWorkspaceError && (_jsx(Alert, { color: "red", icon: _jsx(IconAlertCircle, { size: 16 }), title: "Settings workspace unavailable: groups and path rules are not loading", children: _jsx(Text, { size: "sm", children: status.settingsWorkspaceError }) })), status.schemaIssues && status.schemaIssues.length > 0 && (_jsx(Alert, { color: "yellow", icon: _jsx(IconAlertTriangle, { size: 16 }), title: "Content types this editor version doesn't know", "data-testid": "schema-issues-alert", children: _jsx(Stack, { gap: 4, children: status.schemaIssues.map((issue) => (_jsx(Text, { size: "sm", children: issue.kind === 'unknown-schema' ? (_jsxs(_Fragment, { children: [_jsx(Code, { children: issue.metaFile }), " names ", _jsx(Code, { children: issue.schemaRef }), ", so entry type ", _jsx(Code, { children: issue.entryType }), " is unavailable"] })) : (issue.message) }, `${issue.kind}:${issue.message}`))) }) })), status.workerStatus?.baseHold && _jsx(BaseHoldAlert, { hold: status.workerStatus.baseHold }), status.statusReadError && (_jsxs(Text, { size: "xs", c: "orange", children: ["Warning: could not read worker status (", status.statusReadError, ")"] })), lastGitSync?.tracked && lastGitSync.tracked.diverged.length > 0 && (_jsx(DivergedBranchesAlert, { names: lastGitSync.tracked.diverged })), status.workerStatus?.lastGitSyncError && (_jsxs(Alert, { color: "orange", icon: _jsx(IconAlertCircle, { size: 16 }), title: "Last git sync failed", children: [_jsx(Text, { size: "sm", children: status.workerStatus.lastGitSyncError.message }), _jsxs(Text, { size: "xs", c: "dimmed", mt: 4, children: ["at ", status.workerStatus.lastGitSyncError.at] })] })), versionSkew && (_jsx(Alert, { color: "orange", icon: _jsx(IconAlertCircle, { size: 16 }), title: "API and worker versions differ", "data-testid": "version-skew-warning", children: _jsxs(Text, { size: "sm", children: ["The API runs canopycms ", build.canopycmsVersion, " but the worker runs canopycms", ' ', workerVersion, ". They were deployed from different builds."] }) })), _jsxs(Paper, { withBorder: true, p: "sm", children: [_jsx(Text, { size: "sm", fw: 600, children: "Build" }), _jsxs(Text, { size: "xs", c: "dimmed", "data-testid": "build-api-version", children: ["API: canopycms ", build.canopycmsVersion] }), _jsxs(Text, { size: "xs", c: "dimmed", "data-testid": "build-source-revision", children: ["Source revision:", ' ', build.sourceRevision ? (_jsx(Tooltip, { label: build.sourceRevision, children: _jsx(Code, { children: build.sourceRevision.slice(0, 12) }) })) : ('not set (pass the CANOPY_SOURCE_SHA build arg to the image build)')] }), _jsxs(Text, { size: "xs", c: "dimmed", "data-testid": "build-worker-version", children: ["Worker version: ", workerVersion ? `canopycms ${workerVersion}` : 'unknown'] }), lastShutdown && (_jsxs(Text, { size: "xs", c: lastShutdown.outcome === 'drained' ? 'dimmed' : 'orange', "data-testid": "build-last-shutdown", children: ["Last worker shutdown: ", lastShutdown.reason, " at ", lastShutdown.at, lastShutdown.outcome === 'deadline' &&
|
|
247
|
+
` · drain deadline hit, aborted ${lastShutdown.abandoned?.join(', ') ?? 'in-flight work'}`, lastShutdown.outcome === 'drained' &&
|
|
248
|
+
lastShutdown.drainMs !== undefined &&
|
|
249
|
+
` · drained in ${(lastShutdown.drainMs / 1000).toFixed(1)}s`, lastShutdown.outcome === 'not-drained' && ' (a crash or a forced stop)'] })), lastFatalError?.phase === 'startup' && (_jsxs(Text, { size: "xs", c: "orange", "data-testid": "build-failed-start", children: ["Last start failed at ", lastFatalError.at] })), _jsxs(Text, { size: "xs", c: "dimmed", "data-testid": "build-media", children: ["Media storage:", ' ', status.assetStore.configured ? 'configured' : 'not configured — uploads are disabled'] }), _jsxs(Text, { size: "xs", c: status.imageProcessing.available ? 'dimmed' : 'orange', "data-testid": "build-image-processing", children: ["Image processing:", ' ', status.imageProcessing.available
|
|
250
|
+
? 'available'
|
|
251
|
+
: `unavailable — new editor image sizes and crops fail, and uploads skip decode validation${status.imageProcessing.error ? ` (${status.imageProcessing.error})` : ''}`] })] }), lastGitSync && (_jsxs(Paper, { withBorder: true, p: "sm", children: [_jsx(Text, { size: "sm", fw: 600, children: "Last git sync" }), _jsxs(Text, { size: "xs", c: "dimmed", children: [status.workerStatus?.lastGitSyncAt ?? 'unknown time', " \u00B7 ", lastGitSync.durationMs, "ms \u00B7", ' ', lastGitSync.rebased.length, " rebased \u00B7 ", lastGitSync.skippedDirty.length, " skipped (dirty)", lastGitSync.skippedLocked && lastGitSync.skippedLocked.length > 0
|
|
162
252
|
? ` · ${lastGitSync.skippedLocked.length} skipped (busy: a content write, provisioning or purge)`
|
|
163
|
-
: ''] }), lastGitSync.baseRefresh && (_jsxs(Text, { size: "xs", c: "dimmed", "data-testid": "base-refresh-outcome", children: ["Base branch: ", BASE_REFRESH_LABELS[lastGitSync.baseRefresh.outcome]] })), baseWarning && (_jsx(Text, { size: "xs", c: "orange", style: { whiteSpace: 'pre-line' }, "data-testid": "base-refresh-warning", children: baseWarning })), lastGitSync.failed.length > 0 && (_jsx(Spoiler, { maxHeight: 0, showLabel: `${lastGitSync.failed.length} failed — show details`, hideLabel: "Hide", children: _jsx(Stack, { gap: 4, mt: 4, children: lastGitSync.failed.map((f) => (_jsxs(Text, { size: "xs", c: "
|
|
253
|
+
: ''] }), lastGitSync.baseRefresh && (_jsxs(Text, { size: "xs", c: "dimmed", "data-testid": "base-refresh-outcome", children: ["Base branch: ", BASE_REFRESH_LABELS[lastGitSync.baseRefresh.outcome]] })), baseWarning && (_jsx(Text, { size: "xs", c: "orange", style: { whiteSpace: 'pre-line' }, "data-testid": "base-refresh-warning", children: baseWarning })), lastGitSync.failed.length > 0 && (_jsx(Spoiler, { maxHeight: 0, showLabel: `${lastGitSync.failed.length} failed — show details`, hideLabel: "Hide", children: _jsx(Stack, { gap: 4, mt: 4, children: lastGitSync.failed.map((f) => (_jsxs(Text, { size: "xs", c: "var(--canopy-danger-text)", children: [f.branch, ": ", f.error] }, f.branch))) }) }))] })), _jsx(SimpleGrid, { cols: 5, spacing: "xs", children: ['pending', 'processing', 'completed', 'failed', 'corrupt'].map((key) => (_jsxs(Paper, { withBorder: true, p: "xs", ta: "center", children: [_jsx(Text, { size: "xs", c: "dimmed", tt: "capitalize", children: key }), _jsx(Text, { size: "lg", fw: 700, c: (key === 'failed' || key === 'corrupt') && status.queue[key] > 0
|
|
254
|
+
? 'var(--canopy-danger-text)'
|
|
255
|
+
: undefined, children: status.queue[key] })] }, key))) }), status.queue.oldestPendingAgeMs !== undefined && (_jsxs(Text, { size: "xs", c: "dimmed", children: ["Oldest pending task: ", formatAgeMs(status.queue.oldestPendingAgeMs), " old"] })), _jsxs(Group, { justify: "space-between", align: "center", children: [_jsxs(Text, { size: "xs", c: "dimmed", children: ["Generated at ", status.generatedAt] }), _jsx(Button, { variant: "light", onClick: () => refresh(), loading: statusLoading, children: "Refresh" })] }), _jsx(Text, { size: "xs", c: "dimmed", children: "Data may lag up to ~60s (shared-filesystem caching)" })] }));
|
|
164
256
|
}
|
|
165
257
|
function TasksTab({ health }) {
|
|
166
258
|
const { taskStatus, setTaskStatus, tasks, tasksLoading } = health;
|
|
@@ -170,7 +262,7 @@ function TasksTab({ health }) {
|
|
|
170
262
|
const isEmpty = isCorrupt ? corruptRows.length === 0 : taskRows.length === 0;
|
|
171
263
|
const canDelete = taskStatus === 'pending' || taskStatus === 'failed' || taskStatus === 'corrupt';
|
|
172
264
|
const handleRetryClick = (task) => {
|
|
173
|
-
|
|
265
|
+
openConfirm({
|
|
174
266
|
title: 'Retry task',
|
|
175
267
|
children: _jsx(Text, { size: "sm", children: RETRY_CONFIRM_TEXT }),
|
|
176
268
|
labels: { confirm: 'Retry', cancel: 'Cancel' },
|
|
@@ -179,7 +271,7 @@ function TasksTab({ health }) {
|
|
|
179
271
|
});
|
|
180
272
|
};
|
|
181
273
|
const handleDeleteClick = (status, fileName) => {
|
|
182
|
-
|
|
274
|
+
openConfirm({
|
|
183
275
|
title: 'Delete task file',
|
|
184
276
|
children: _jsx(Text, { size: "sm", children: deleteConfirmText(status) }),
|
|
185
277
|
labels: { confirm: 'Delete', cancel: 'Cancel' },
|
|
@@ -191,15 +283,15 @@ function TasksTab({ health }) {
|
|
|
191
283
|
if (isAdminTaskStatus(value))
|
|
192
284
|
setTaskStatus(value);
|
|
193
285
|
} }), tasksLoading && taskRows.length === 0 && corruptRows.length === 0 ? (_jsxs(Group, { justify: "center", py: "xl", children: [_jsx(Loader, { size: "sm" }), _jsx(Text, { size: "sm", c: "dimmed", children: "Loading tasks..." })] })) : isEmpty ? (_jsxs(Text, { size: "sm", c: "dimmed", py: "md", children: ["No ", taskStatus, " tasks."] })) : (_jsx(Table.ScrollContainer, { minWidth: 600, children: _jsxs(Table, { children: [_jsx(Table.Thead, { children: _jsx(Table.Tr, { children: isCorrupt ? (_jsxs(_Fragment, { children: [_jsx(Table.Th, { children: "File" }), _jsx(Table.Th, { children: "Size" }), _jsx(Table.Th, { children: "Modified" }), _jsx(Table.Th, { children: "Raw snippet" }), _jsx(Table.Th, { children: "Actions" })] })) : (_jsxs(_Fragment, { children: [_jsx(Table.Th, { children: "ID" }), _jsx(Table.Th, { children: "Action" }), _jsx(Table.Th, { children: "Created" }), _jsx(Table.Th, { children: "Retries" }), _jsx(Table.Th, { children: "Error" }), _jsx(Table.Th, { children: "Actions" })] })) }) }), _jsx(Table.Tbody, { children: isCorrupt
|
|
194
|
-
? corruptRows.map((file) => (_jsxs(Table.Tr, { children: [_jsx(Table.Td, { children: _jsx("span", { title: file.fileName, children: truncate(file.fileName, 30) }) }), _jsxs(Table.Td, { children: [file.size, " bytes"] }), _jsx(Table.Td, { children: file.mtime }), _jsx(Table.Td, { children: _jsx(Code, { block: true, style: { maxWidth: 320, whiteSpace: 'pre-wrap' }, children: file.rawSnippet }) }), _jsx(Table.Td, { children: _jsx(Button, {
|
|
195
|
-
: taskRows.map((task) => (_jsxs(Table.Tr, { children: [_jsx(Table.Td, { children: _jsx("span", { title: task.id, children: truncate(task.id, 12) }) }), _jsx(Table.Td, { children: task.action }), _jsx(Table.Td, { children: task.createdAt }), _jsx(Table.Td, { children: task.retryCount ?? 0 }), _jsx(Table.Td, { children: task.error ? (_jsx(Tooltip, { label: task.error, multiline: true, maw: 400, children: _jsx(Text, { size: "xs", style: { cursor: 'help' }, children: truncate(task.error, 50) }) })) : (_jsx(Text, { size: "xs", c: "dimmed", children: "\u2014" })) }), _jsx(Table.Td, { children: _jsxs(Group, { gap: "xs", children: [taskStatus === 'failed' && (_jsx(Button, {
|
|
286
|
+
? corruptRows.map((file) => (_jsxs(Table.Tr, { children: [_jsx(Table.Td, { children: _jsx("span", { title: file.fileName, children: truncate(file.fileName, 30) }) }), _jsxs(Table.Td, { children: [file.size, " bytes"] }), _jsx(Table.Td, { children: file.mtime }), _jsx(Table.Td, { children: _jsx(Code, { block: true, style: { maxWidth: 320, whiteSpace: 'pre-wrap' }, children: file.rawSnippet }) }), _jsx(Table.Td, { children: _jsx(Button, { variant: "light", color: "red", "data-testid": `delete-task-${file.fileName}`, onClick: () => handleDeleteClick('corrupt', file.fileName), children: "Delete" }) })] }, file.fileName)))
|
|
287
|
+
: taskRows.map((task) => (_jsxs(Table.Tr, { children: [_jsx(Table.Td, { children: _jsx("span", { title: task.id, children: truncate(task.id, 12) }) }), _jsx(Table.Td, { children: task.action }), _jsx(Table.Td, { children: task.createdAt }), _jsx(Table.Td, { children: task.retryCount ?? 0 }), _jsx(Table.Td, { children: task.error ? (_jsx(Tooltip, { label: task.error, multiline: true, maw: 400, children: _jsx(Text, { size: "xs", style: { cursor: 'help' }, children: truncate(task.error, 50) }) })) : (_jsx(Text, { size: "xs", c: "dimmed", children: "\u2014" })) }), _jsx(Table.Td, { children: _jsxs(Group, { gap: "xs", children: [taskStatus === 'failed' && (_jsx(Button, { variant: "light", "data-testid": `retry-task-${task.id}`, onClick: () => handleRetryClick(task), children: "Retry" })), canDelete && (_jsx(Button, { variant: "light", color: "red", "data-testid": `delete-task-${task.id}`, onClick: () => handleDeleteClick(taskStatus, `${task.id}.json`), children: "Delete" }))] }) })] }, task.id))) })] }) }))] }));
|
|
196
288
|
}
|
|
197
289
|
function BranchesTab({ health }) {
|
|
198
290
|
const { branchHealth, branchHealthLoading } = health;
|
|
199
291
|
const entries = branchHealth?.entries ?? [];
|
|
200
292
|
const baseWarning = baseRefreshWarning(health.status?.workerStatus?.lastGitSync?.baseRefresh);
|
|
201
293
|
const handleMarkMergedClick = (branchName) => {
|
|
202
|
-
|
|
294
|
+
openConfirm({
|
|
203
295
|
title: 'Mark branch as merged',
|
|
204
296
|
children: _jsx(Text, { size: "sm", children: MARK_MERGED_CONFIRM_TEXT }),
|
|
205
297
|
labels: { confirm: 'Mark merged', cancel: 'Cancel' },
|
|
@@ -208,7 +300,7 @@ function BranchesTab({ health }) {
|
|
|
208
300
|
});
|
|
209
301
|
};
|
|
210
302
|
const handleRepairClick = (dirName) => {
|
|
211
|
-
|
|
303
|
+
openConfirm({
|
|
212
304
|
title: 'Repair metadata',
|
|
213
305
|
children: _jsx(Text, { size: "sm", children: REPAIR_CONFIRM_TEXT }),
|
|
214
306
|
labels: { confirm: 'Repair', cancel: 'Cancel' },
|
|
@@ -217,7 +309,7 @@ function BranchesTab({ health }) {
|
|
|
217
309
|
});
|
|
218
310
|
};
|
|
219
311
|
const handlePurgeClick = (dirName) => {
|
|
220
|
-
|
|
312
|
+
openConfirm({
|
|
221
313
|
title: 'Purge directory',
|
|
222
314
|
children: _jsx(Text, { size: "sm", children: PURGE_CONFIRM_TEXT }),
|
|
223
315
|
labels: { confirm: 'Purge', cancel: 'Cancel' },
|
|
@@ -225,15 +317,53 @@ function BranchesTab({ health }) {
|
|
|
225
317
|
onConfirm: () => health.purgeDir(dirName),
|
|
226
318
|
});
|
|
227
319
|
};
|
|
320
|
+
// Its own dialog, reachable only from the duplicate badge, whose confirm
|
|
321
|
+
// calls only the duplicate repair: Purge sits on neighbouring rows and
|
|
322
|
+
// trashes a whole branch directory.
|
|
323
|
+
const handleDuplicateIdsClick = (dirName, duplicates) => {
|
|
324
|
+
openConfirm({
|
|
325
|
+
title: 'Fix duplicate content IDs',
|
|
326
|
+
children: _jsx(DuplicateRepairDetails, { dirName: dirName, duplicates: duplicates }),
|
|
327
|
+
labels: { confirm: 'Archive duplicates', cancel: 'Cancel' },
|
|
328
|
+
confirmProps: { color: 'orange' },
|
|
329
|
+
onConfirm: () => health.repairDuplicateIds(dirName),
|
|
330
|
+
});
|
|
331
|
+
};
|
|
332
|
+
const healthyDirNames = entries.filter((e) => e.kind === 'healthy').map((e) => e.dirName);
|
|
333
|
+
const duplicateSummary = duplicateScanSummary(health, healthyDirNames);
|
|
228
334
|
if (branchHealthLoading && entries.length === 0) {
|
|
229
335
|
return (_jsxs(Group, { justify: "center", py: "xl", children: [_jsx(Loader, { size: "sm" }), _jsx(Text, { size: "sm", c: "dimmed", children: "Loading branch health..." })] }));
|
|
230
336
|
}
|
|
231
337
|
if (entries.length === 0) {
|
|
232
338
|
return (_jsx(Text, { size: "sm", c: "dimmed", py: "md", children: "No branch directories found." }));
|
|
233
339
|
}
|
|
234
|
-
return (_jsx(Table.ScrollContainer, { minWidth: 700, children: _jsxs(Table, { children: [_jsx(Table.Thead, { children: _jsxs(Table.Tr, { children: [_jsx(Table.Th, { children: "Name" }), _jsx(Table.Th, { children: "Status" }), _jsx(Table.Th, { children: "PR" }), _jsx(Table.Th, { children: "Sync" }), _jsx(Table.Th, { children: "Warnings" }), _jsx(Table.Th, { children: "Updated" }), _jsx(Table.Th, { children: "Actions" })] }) }), _jsx(Table.Tbody, { children: entries.map((entry) => (_jsx(BranchHealthRow, { entry: entry, baseWarning: entry.isBaseBranch ? baseWarning : null,
|
|
340
|
+
return (_jsxs(Stack, { gap: "xs", children: [_jsxs(Group, { justify: "space-between", align: "center", wrap: "nowrap", children: [_jsx(Text, { size: "xs", c: duplicateSummary.color, "data-testid": "duplicate-id-summary", children: duplicateSummary.text }), _jsx(Button, { size: "compact-xs", variant: "subtle", loading: health.duplicateIdScanLoading, onClick: () => health.checkDuplicateIds(), children: "Check again" })] }), _jsx(Table.ScrollContainer, { minWidth: 700, children: _jsxs(Table, { children: [_jsx(Table.Thead, { children: _jsxs(Table.Tr, { children: [_jsx(Table.Th, { children: "Name" }), _jsx(Table.Th, { children: "Status" }), _jsx(Table.Th, { children: "PR" }), _jsx(Table.Th, { children: "Sync" }), _jsx(Table.Th, { children: "Warnings" }), _jsx(Table.Th, { children: "Updated" }), _jsx(Table.Th, { children: "Actions" })] }) }), _jsx(Table.Tbody, { children: entries.map((entry) => (_jsx(BranchHealthRow, { entry: entry, baseWarning: entry.isBaseBranch ? baseWarning : null, duplicateIdScan: health.duplicateIdScan
|
|
341
|
+
? (health.duplicateIdScan.byDir[entry.dirName] ?? null)
|
|
342
|
+
: undefined, onMarkMerged: handleMarkMergedClick, onRepair: handleRepairClick, onPurge: handlePurgeClick, onDuplicateIds: handleDuplicateIdsClick }, entry.dirName))) })] }) })] }));
|
|
343
|
+
}
|
|
344
|
+
function DuplicateRepairDetails({ dirName, duplicates, }) {
|
|
345
|
+
return (_jsxs(Stack, { gap: "sm", children: [_jsxs(Text, { size: "sm", children: ["Branch ", _jsx(Code, { children: dirName })] }), _jsx(Text, { size: "sm", children: DUPLICATE_REPAIR_CONFIRM_TEXT }), duplicates.map((d) => (_jsxs(Paper, { withBorder: true, p: "xs", radius: "sm", "data-testid": `duplicate-id-${d.id}`, children: [_jsxs(Text, { size: "xs", fw: 600, children: ["Content ID ", _jsx(Code, { children: d.id })] }), _jsxs(Text, { size: "xs", children: ["Keep: ", _jsx(Code, { children: d.keptPath })] }), d.droppedPaths.map((p) => (_jsxs(Text, { size: "xs", children: ["Archive: ", _jsx(Code, { children: p })] }, p)))] }, d.id))), _jsx(Text, { size: "xs", c: "dimmed", children: DUPLICATE_REPAIR_EDITOR_NOTE })] }));
|
|
346
|
+
}
|
|
347
|
+
/** "2d ago, 1/1/2026, 9:00:00 AM": inside a tooltip, so the absolute date sits beside it. */
|
|
348
|
+
const firstFailed = (iso) => {
|
|
349
|
+
const date = new Date(iso);
|
|
350
|
+
return Number.isNaN(date.getTime()) ? iso : `${formatRelativeTime(iso)}, ${date.toLocaleString()}`;
|
|
351
|
+
};
|
|
352
|
+
/**
|
|
353
|
+
* A detail that only a Tooltip would show: hidden text for assistive tech, plus a Tooltip that
|
|
354
|
+
* opens on hover, focus and touch. The trigger is a focusable span, not a button: it does nothing
|
|
355
|
+
* when pressed, so it must not be announced as one. A trigger with no text of its own, such as an
|
|
356
|
+
* icon, takes its name from `name`.
|
|
357
|
+
*/
|
|
358
|
+
function TooltipDetail({ detail, name, maw = 320, preLine = false, children, }) {
|
|
359
|
+
const detailId = useId();
|
|
360
|
+
return (_jsxs(_Fragment, { children: [_jsx(Tooltip
|
|
361
|
+
// A sighted user sees only the icon, so the tooltip names it too.
|
|
362
|
+
, {
|
|
363
|
+
// A sighted user sees only the icon, so the tooltip names it too.
|
|
364
|
+
label: name ? `${name}: ${detail}` : detail, multiline: true, maw: maw, style: preLine ? { whiteSpace: 'pre-line' } : undefined, events: { hover: true, focus: true, touch: true }, children: _jsx(Box, { component: "span", tabIndex: 0, role: name ? 'img' : undefined, "aria-label": name, "aria-describedby": detailId, className: "mantine-focus-auto", children: children }) }), _jsx(VisuallyHidden, { id: detailId, children: detail })] }));
|
|
235
365
|
}
|
|
236
|
-
function BranchHealthRow({ entry, baseWarning, onMarkMerged, onRepair, onPurge, }) {
|
|
366
|
+
function BranchHealthRow({ entry, baseWarning, duplicateIdScan, onMarkMerged, onRepair, onPurge, onDuplicateIds, }) {
|
|
237
367
|
if (entry.kind === 'healthy' && entry.branch) {
|
|
238
368
|
const b = entry.branch;
|
|
239
369
|
// Mirrors the rebase loop's skip logic (worker/rebase.ts): the worker
|
|
@@ -243,17 +373,17 @@ function BranchHealthRow({ entry, baseWarning, onMarkMerged, onRepair, onPurge,
|
|
|
243
373
|
// default instead of silently hiding them.
|
|
244
374
|
const showRebaseFailure = !['submitted', 'approved', 'archived'].includes(b.status) && !!b.rebaseFailure;
|
|
245
375
|
const canMarkMerged = (b.status === 'submitted' || b.status === 'approved') && !!b.pullRequestNumber;
|
|
246
|
-
return (_jsxs(Table.Tr, { children: [_jsx(Table.Td, { children: _jsxs(Group, { gap: 4, wrap: "nowrap", children: [_jsx(Text, { size: "sm", children: entry.dirName }), entry.isBaseBranch && (_jsx(Badge, { size: "xs", color: "gray", variant: "outline", children: "
|
|
247
|
-
? `${b.syncFailureReason} — retry from the Tasks tab`
|
|
248
|
-
: 'GitHub sync failed — retry from the Tasks tab',
|
|
249
|
-
|
|
250
|
-
|
|
376
|
+
return (_jsxs(Table.Tr, { children: [_jsx(Table.Td, { children: _jsxs(Group, { gap: 4, wrap: "nowrap", children: [_jsx(Text, { size: "sm", children: entry.dirName }), entry.isBaseBranch && (_jsx(Badge, { size: "xs", color: "gray", variant: "outline", children: "Base" }))] }) }), _jsx(Table.Td, { children: _jsx(Badge, { color: branchStatusPresentation(b.status)?.color ?? 'neutral', variant: "light", children: branchStatusPresentation(b.status)?.label }) }), _jsx(Table.Td, { children: b.pullRequestNumber ? (_jsxs(Text, { size: "xs", c: "blue", component: "a", href: b.pullRequestUrl, target: "_blank", rel: "noopener noreferrer", style: { textDecoration: 'underline' }, children: ["PR #", b.pullRequestNumber, b.pullRequestState ? ` (${b.pullRequestState})` : ''] })) : (_jsx(Text, { size: "xs", c: "dimmed", children: "\u2014" })) }), _jsx(Table.Td, { children: b.syncStatus === 'sync-failed' ? (_jsx(TooltipDetail, { detail: b.syncFailureReason
|
|
377
|
+
? `${b.syncFailureReason} — retry from the "Tasks" tab`
|
|
378
|
+
: 'GitHub sync failed — retry from the "Tasks" tab', children: _jsx(Badge, { color: "red", variant: "light", children: "Sync failed" }) })) : b.syncStatus === 'pending-sync' ? (_jsx(Badge, { color: "gray", variant: "light", children: "Pending sync" })) : null }), _jsx(Table.Td, { children: _jsxs(Group, { gap: 6, wrap: "nowrap", children: [!!b.conflictFiles?.length && (_jsxs(Badge, { color: "orange", variant: "light", children: [b.conflictFiles.length, " conflict", b.conflictFiles.length === 1 ? '' : 's'] })), showRebaseFailure && b.rebaseFailure && (_jsx(TooltipDetail, { name: "Rebase failing", detail: `${b.rebaseFailure.message} (first failed ${firstFailed(b.rebaseFailure.firstAt)})`, children: _jsx(ThemeIcon, { color: "yellow", variant: "light", size: "sm", radius: "xl", "data-testid": `rebase-failure-${entry.dirName}`, children: _jsx(IconAlertTriangle, { size: 12 }) }) })), baseWarning && (_jsx(TooltipDetail, { name: "Base branch refresh failing", detail: baseWarning, maw: 420, preLine: true, children: _jsx(ThemeIcon, { color: "yellow", variant: "light", size: "sm", radius: "xl", "data-testid": `base-refresh-warning-${entry.dirName}`, children: _jsx(IconAlertTriangle, { size: 12 }) }) })), duplicateIdScan?.state === 'found' && (_jsx(Tooltip, { label: "Review and fix", children: _jsx(Badge, { component: "button", type: "button", color: "orange", variant: "light", style: { cursor: 'pointer' }, "data-testid": `duplicate-content-ids-${entry.dirName}`, onClick: () => onDuplicateIds(entry.dirName, duplicateIdScan.duplicates), children: plural(duplicateIdScan.duplicates.length, 'duplicate ID') }) })), duplicateIdScan !== undefined &&
|
|
379
|
+
duplicateIdScan?.state !== 'found' &&
|
|
380
|
+
duplicateIdScan?.state !== 'none' && (_jsx(Tooltip, { label: duplicateUncheckedReason(duplicateIdScan), multiline: true, maw: 320, children: _jsx(Badge, { color: "gray", variant: "outline", "data-testid": `duplicate-ids-unchecked-${entry.dirName}`, children: "IDs not checked" }) }))] }) }), _jsx(Table.Td, { children: _jsx(Text, { size: "xs", c: "dimmed", children: b.updatedAt }) }), _jsx(Table.Td, { children: canMarkMerged && (_jsx(Button, { variant: "light", "data-testid": `mark-merged-${b.name}`, onClick: () => onMarkMerged(b.name), children: "Mark merged" })) })] }));
|
|
251
381
|
}
|
|
252
382
|
if (entry.kind === 'corrupt-metadata') {
|
|
253
383
|
const purgeGate = purgeGateFor(entry);
|
|
254
|
-
return (_jsxs(Table.Tr, { style: { backgroundColor: 'var(--mantine-color-red-light)' }, children: [_jsx(Table.Td, { children: entry.dirName }), _jsx(Table.Td, { children: _jsx(Badge, { color: "red", variant: "light", children: "
|
|
384
|
+
return (_jsxs(Table.Tr, { style: { backgroundColor: 'var(--mantine-color-red-light)' }, children: [_jsx(Table.Td, { children: entry.dirName }), _jsx(Table.Td, { children: _jsx(Badge, { color: "red", variant: "light", children: "Corrupt metadata" }) }), _jsx(Table.Td, { colSpan: 3, children: _jsx(Tooltip, { label: entry.parseError, multiline: true, maw: 400, children: _jsx(Text, { size: "xs", c: "var(--canopy-danger-text)", children: truncate(entry.parseError ?? 'Unknown parse error', 60) }) }) }), _jsx(Table.Td, { children: _jsx(Text, { size: "xs", c: "dimmed", children: entry.metaMtime ?? '—' }) }), _jsx(Table.Td, { children: _jsxs(Group, { gap: "xs", wrap: "nowrap", children: [_jsx(Button, { variant: "light", "data-testid": `repair-dir-${entry.dirName}`, onClick: () => onRepair(entry.dirName), children: "Repair" }), _jsx(PurgeButton, { entry: entry, gate: purgeGate, onPurge: onPurge })] }) })] }));
|
|
255
385
|
}
|
|
256
386
|
// orphan
|
|
257
387
|
const purgeGate = purgeGateFor(entry);
|
|
258
|
-
return (_jsxs(Table.Tr, { style: { opacity: 0.65 }, children: [_jsx(Table.Td, { children: entry.dirName }), _jsx(Table.Td, { children: _jsx(Badge, { color: "gray", variant: "outline", children: "
|
|
388
|
+
return (_jsxs(Table.Tr, { style: { opacity: 0.65 }, children: [_jsx(Table.Td, { children: entry.dirName }), _jsx(Table.Td, { children: _jsx(Badge, { color: "gray", variant: "outline", children: "Orphan" }) }), _jsx(Table.Td, { colSpan: 3, children: _jsxs(Text, { size: "xs", c: "dimmed", children: [entry.ageMs !== undefined ? `${formatAgeMs(entry.ageMs)} old` : 'unknown age', " \u00B7", ' ', entry.hasGitDir ? 'has .git' : 'no .git'] }) }), _jsx(Table.Td, { children: _jsx(Text, { size: "xs", c: "dimmed", children: entry.dirMtime ?? '—' }) }), _jsx(Table.Td, { children: _jsx(PurgeButton, { entry: entry, gate: purgeGate, onPurge: onPurge }) })] }));
|
|
259
389
|
}
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import type { AdminStatusData, AdminTasksData, ListAdminTasksParams, DeleteTaskParams } from '../../api/admin.js';
|
|
2
2
|
import type { BranchHealthData } from '../../api/admin-branch-health.js';
|
|
3
|
+
import type { DuplicateIdScan } from '../../branch-health.js';
|
|
3
4
|
export type AdminTaskStatus = ListAdminTasksParams['status'];
|
|
4
5
|
export type DeletableTaskStatus = DeleteTaskParams['status'];
|
|
6
|
+
/** The last completed duplicate-ID scan (`GET /admin/branch-health?duplicates=1`). */
|
|
7
|
+
interface DuplicateIdScanResult {
|
|
8
|
+
/** Healthy branches only, by dirName. A healthy row missing here was not scanned. */
|
|
9
|
+
byDir: Record<string, DuplicateIdScan>;
|
|
10
|
+
/** The server's time budget ran out before every healthy branch was scanned. */
|
|
11
|
+
truncated: boolean;
|
|
12
|
+
generatedAt: string;
|
|
13
|
+
}
|
|
5
14
|
export interface UseSystemHealthOptions {
|
|
6
15
|
/**
|
|
7
16
|
* Whether the System Health panel is currently open. Data loads (and
|
|
@@ -25,6 +34,14 @@ export interface UseSystemHealthReturn {
|
|
|
25
34
|
setTaskStatus: (status: AdminTaskStatus) => void;
|
|
26
35
|
branchHealth: BranchHealthData | null;
|
|
27
36
|
branchHealthLoading: boolean;
|
|
37
|
+
/** Null until a scan completes, and after a scan request fails. */
|
|
38
|
+
duplicateIdScan: DuplicateIdScanResult | null;
|
|
39
|
+
duplicateIdScanLoading: boolean;
|
|
40
|
+
duplicateIdScanError: string | null;
|
|
41
|
+
/** Runs the duplicate-ID scan. */
|
|
42
|
+
checkDuplicateIds: () => Promise<void>;
|
|
43
|
+
/** Archives the branch's duplicate files, then re-runs the scan. */
|
|
44
|
+
repairDuplicateIds: (dirName: string) => Promise<void>;
|
|
28
45
|
/** Last fetch error across status/tasks/branchHealth, if any. */
|
|
29
46
|
error: string | null;
|
|
30
47
|
/** Refetches status, tasks (at the current taskStatus), and branchHealth. */
|
|
@@ -36,3 +53,4 @@ export interface UseSystemHealthReturn {
|
|
|
36
53
|
markMerged: (branchName: string) => Promise<void>;
|
|
37
54
|
}
|
|
38
55
|
export declare function useSystemHealth(options: UseSystemHealthOptions): UseSystemHealthReturn;
|
|
56
|
+
export {};
|
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
* then refresh()), but also polls every 30s while open (cleared on
|
|
7
7
|
* close/unmount) since queue depth and worker liveness go stale quickly and
|
|
8
8
|
* the panel has no other way to catch a worker recovering or a task finishing.
|
|
9
|
+
*
|
|
10
|
+
* The duplicate-ID scan is the exception: it reads every healthy branch's whole
|
|
11
|
+
* content tree, so it runs on open, on request, and after a duplicate repair,
|
|
12
|
+
* never on the poll.
|
|
9
13
|
*/
|
|
10
14
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
11
15
|
import { notifications } from '@mantine/notifications';
|
|
@@ -28,6 +32,9 @@ export function useSystemHealth(options) {
|
|
|
28
32
|
const [taskStatus, setTaskStatusState] = useState('failed');
|
|
29
33
|
const [branchHealth, setBranchHealth] = useState(null);
|
|
30
34
|
const [branchHealthLoading, setBranchHealthLoading] = useState(false);
|
|
35
|
+
const [duplicateIdScan, setDuplicateIdScan] = useState(null);
|
|
36
|
+
const [duplicateIdScanLoading, setDuplicateIdScanLoading] = useState(false);
|
|
37
|
+
const [duplicateIdScanError, setDuplicateIdScanError] = useState(null);
|
|
31
38
|
const [error, setError] = useState(null);
|
|
32
39
|
// refresh() has no status argument, so it needs the CURRENT taskStatus
|
|
33
40
|
// without depending on it (that would tear down/rebuild the poll interval
|
|
@@ -74,7 +81,7 @@ export function useSystemHealth(options) {
|
|
|
74
81
|
const fetchBranchHealth = useCallback(async () => {
|
|
75
82
|
setBranchHealthLoading(true);
|
|
76
83
|
try {
|
|
77
|
-
const result = await apiClient.admin.branchHealth();
|
|
84
|
+
const result = await apiClient.admin.branchHealth({});
|
|
78
85
|
if (!result.ok)
|
|
79
86
|
throw new Error(result.error || 'Failed to load branch health');
|
|
80
87
|
setBranchHealth(result.data ?? null);
|
|
@@ -87,6 +94,43 @@ export function useSystemHealth(options) {
|
|
|
87
94
|
setBranchHealthLoading(false);
|
|
88
95
|
}
|
|
89
96
|
}, [apiClient]);
|
|
97
|
+
// Only the latest scan request may write state: a repair's re-scan can
|
|
98
|
+
// overlap the on-open scan, and the older response would bring back
|
|
99
|
+
// duplicates the repair just archived.
|
|
100
|
+
const duplicateScanSeq = useRef(0);
|
|
101
|
+
const checkDuplicateIds = useCallback(async () => {
|
|
102
|
+
const seq = ++duplicateScanSeq.current;
|
|
103
|
+
setDuplicateIdScanLoading(true);
|
|
104
|
+
try {
|
|
105
|
+
const result = await apiClient.admin.branchHealth({ duplicates: '1' });
|
|
106
|
+
if (seq !== duplicateScanSeq.current)
|
|
107
|
+
return;
|
|
108
|
+
if (!result.ok || !result.data?.duplicateIdScan) {
|
|
109
|
+
throw new Error(result.error || 'Failed to check for duplicate content IDs');
|
|
110
|
+
}
|
|
111
|
+
const byDir = {};
|
|
112
|
+
for (const entry of result.data.entries) {
|
|
113
|
+
if (entry.duplicateIdScan)
|
|
114
|
+
byDir[entry.dirName] = entry.duplicateIdScan;
|
|
115
|
+
}
|
|
116
|
+
setDuplicateIdScan({
|
|
117
|
+
byDir,
|
|
118
|
+
truncated: result.data.duplicateIdScan.truncated,
|
|
119
|
+
generatedAt: result.data.generatedAt,
|
|
120
|
+
});
|
|
121
|
+
setDuplicateIdScanError(null);
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
if (seq !== duplicateScanSeq.current)
|
|
125
|
+
return;
|
|
126
|
+
setDuplicateIdScan(null);
|
|
127
|
+
setDuplicateIdScanError(err instanceof Error ? err.message : 'Failed to check for duplicate content IDs');
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
if (seq === duplicateScanSeq.current)
|
|
131
|
+
setDuplicateIdScanLoading(false);
|
|
132
|
+
}
|
|
133
|
+
}, [apiClient]);
|
|
90
134
|
const refresh = useCallback(async () => {
|
|
91
135
|
await Promise.all([fetchStatus(), fetchTasks(taskStatusRef.current), fetchBranchHealth()]);
|
|
92
136
|
}, [fetchStatus, fetchTasks, fetchBranchHealth]);
|
|
@@ -101,6 +145,7 @@ export function useSystemHealth(options) {
|
|
|
101
145
|
if (!options.isOpen)
|
|
102
146
|
return;
|
|
103
147
|
refresh();
|
|
148
|
+
void checkDuplicateIds();
|
|
104
149
|
const interval = setInterval(() => {
|
|
105
150
|
refresh();
|
|
106
151
|
}, POLL_INTERVAL_MS);
|
|
@@ -168,6 +213,25 @@ export function useSystemHealth(options) {
|
|
|
168
213
|
notifications.show({ message, color: 'red' });
|
|
169
214
|
}
|
|
170
215
|
}, [apiClient, refresh]);
|
|
216
|
+
const repairDuplicateIds = useCallback(async (dirName) => {
|
|
217
|
+
try {
|
|
218
|
+
const result = await apiClient.admin.repairContentDuplicates({ dirName });
|
|
219
|
+
if (!result.ok)
|
|
220
|
+
throw new Error(result.error || 'Failed to archive duplicate files');
|
|
221
|
+
const archived = (result.data?.resolved ?? []).reduce((n, r) => n + r.archivedAs.length, 0);
|
|
222
|
+
notifications.show({
|
|
223
|
+
message: `Archived ${archived} duplicate file${archived === 1 ? '' : 's'}`,
|
|
224
|
+
color: 'green',
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
catch (err) {
|
|
228
|
+
const message = err instanceof Error ? err.message : 'Failed to archive duplicate files';
|
|
229
|
+
notifications.show({ message, color: 'red' });
|
|
230
|
+
}
|
|
231
|
+
// Re-scan on failure too: a 409 or a partial repair leaves the branch in
|
|
232
|
+
// a state the last scan no longer describes.
|
|
233
|
+
await checkDuplicateIds();
|
|
234
|
+
}, [apiClient, checkDuplicateIds]);
|
|
171
235
|
const markMerged = useCallback(async (branchName) => {
|
|
172
236
|
try {
|
|
173
237
|
const result = await apiClient.workflow.markMerged({ branch: branchName });
|
|
@@ -191,6 +255,11 @@ export function useSystemHealth(options) {
|
|
|
191
255
|
setTaskStatus,
|
|
192
256
|
branchHealth,
|
|
193
257
|
branchHealthLoading,
|
|
258
|
+
duplicateIdScan,
|
|
259
|
+
duplicateIdScanLoading,
|
|
260
|
+
duplicateIdScanError,
|
|
261
|
+
checkDuplicateIds,
|
|
262
|
+
repairDuplicateIds,
|
|
194
263
|
error,
|
|
195
264
|
refresh,
|
|
196
265
|
retryTask,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { BranchStatus } from '../types.js';
|
|
2
|
+
export interface BranchStatusPresentation {
|
|
3
|
+
label: string;
|
|
4
|
+
color: string;
|
|
5
|
+
variant: 'light' | 'outline';
|
|
6
|
+
}
|
|
7
|
+
/** @internal Exported for tests. */
|
|
8
|
+
export declare const PROTECTED_BRANCH_PRESENTATION: BranchStatusPresentation;
|
|
9
|
+
/** @internal Exported for tests. */
|
|
10
|
+
export declare const BRANCH_STATUS_PRESENTATION: Record<BranchStatus, BranchStatusPresentation>;
|
|
11
|
+
/**
|
|
12
|
+
* The badge for a branch: the protected base branch reads "Protected" whatever its workflow
|
|
13
|
+
* status, so a base branch never advertises a state it cannot act on. `status` is a string
|
|
14
|
+
* because server data is untyped at runtime; an unrecognised one shows as itself, neutrally.
|
|
15
|
+
* Returns undefined when there is nothing to show.
|
|
16
|
+
*/
|
|
17
|
+
export declare function branchStatusPresentation(status: string | undefined, isProtected?: boolean): BranchStatusPresentation | undefined;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** @internal Exported for tests. */
|
|
2
|
+
export const PROTECTED_BRANCH_PRESENTATION = {
|
|
3
|
+
label: 'Protected',
|
|
4
|
+
color: 'neutral',
|
|
5
|
+
variant: 'outline',
|
|
6
|
+
};
|
|
7
|
+
/** @internal Exported for tests. */
|
|
8
|
+
export const BRANCH_STATUS_PRESENTATION = {
|
|
9
|
+
editing: { label: 'Editing', color: 'brand', variant: 'light' },
|
|
10
|
+
submitted: { label: 'In review', color: 'green', variant: 'light' },
|
|
11
|
+
approved: { label: 'Approved', color: 'teal', variant: 'light' },
|
|
12
|
+
archived: { label: 'Archived', color: 'gray', variant: 'light' },
|
|
13
|
+
};
|
|
14
|
+
const isKnownStatus = (status) => Object.prototype.hasOwnProperty.call(BRANCH_STATUS_PRESENTATION, status);
|
|
15
|
+
/**
|
|
16
|
+
* The badge for a branch: the protected base branch reads "Protected" whatever its workflow
|
|
17
|
+
* status, so a base branch never advertises a state it cannot act on. `status` is a string
|
|
18
|
+
* because server data is untyped at runtime; an unrecognised one shows as itself, neutrally.
|
|
19
|
+
* Returns undefined when there is nothing to show.
|
|
20
|
+
*/
|
|
21
|
+
export function branchStatusPresentation(status, isProtected = false) {
|
|
22
|
+
if (isProtected)
|
|
23
|
+
return PROTECTED_BRANCH_PRESENTATION;
|
|
24
|
+
if (!status)
|
|
25
|
+
return undefined;
|
|
26
|
+
if (isKnownStatus(status))
|
|
27
|
+
return BRANCH_STATUS_PRESENTATION[status];
|
|
28
|
+
return { label: status, color: 'gray', variant: 'light' };
|
|
29
|
+
}
|
|
@@ -1,16 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one spelling of a field path, `blocks[0].title`, for the form's fields, the preview's
|
|
3
|
+
* marks and comment threads. A digits-only segment is always a list index (`[0]` or `.0`):
|
|
4
|
+
* schemas refuse field names `isPathFieldName` rejects, since no path could spell them.
|
|
5
|
+
*/
|
|
1
6
|
export type CanopyPathSegment = string | number;
|
|
7
|
+
export declare const isPathFieldName: (name: string) => boolean;
|
|
2
8
|
/**
|
|
3
9
|
* Convert a list of path segments into the canonical CanopyCMS path string.
|
|
4
10
|
* Arrays are rendered with bracket notation (e.g., blocks[0].title).
|
|
5
11
|
*/
|
|
6
|
-
export declare const formatCanopyPath: (segments: CanopyPathSegment[]) => string;
|
|
12
|
+
export declare const formatCanopyPath: (segments: readonly CanopyPathSegment[]) => string;
|
|
7
13
|
/**
|
|
8
14
|
* Parse a CanopyCMS path string into segments. Supports bracketed array
|
|
9
15
|
* indices and dotted segments (e.g., blocks.0.title or blocks[0].title).
|
|
10
16
|
*/
|
|
11
17
|
export declare const parseCanopyPath: (path: string) => CanopyPathSegment[];
|
|
12
|
-
/**
|
|
13
|
-
|
|
14
|
-
* canonical bracketed representation.
|
|
15
|
-
*/
|
|
16
|
-
export declare const normalizeCanopyPath: (input: string | CanopyPathSegment[]) => string;
|
|
18
|
+
/** The canonical string for a path; segments are re-parsed, so `['a', '0']` is `a[0]`. */
|
|
19
|
+
export declare const normalizeCanopyPath: (input: string | readonly CanopyPathSegment[]) => string;
|